mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-18 09:13:28 +00:00
Compare commits
35 Commits
AC-1743/up
...
ac-1743
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c521987e | ||
|
|
cd71a7a0ae | ||
|
|
c0bdb67ce9 | ||
|
|
ae8ce6643d | ||
|
|
9e131dc9c8 | ||
|
|
ebe5eaedb2 | ||
|
|
a098620b95 | ||
|
|
d222a26eb6 | ||
|
|
d50e10bb55 | ||
|
|
aca1daa58f | ||
|
|
5bd63c6d90 | ||
|
|
f71f6db0e5 | ||
|
|
8ef50234c8 | ||
|
|
caa846f065 | ||
|
|
6c553a020c | ||
|
|
3c2caf0ef5 | ||
|
|
74b7287ba7 | ||
|
|
48ee5eb4da | ||
|
|
ce80c031db | ||
|
|
0ec615e16b | ||
|
|
3a2f0988a5 | ||
|
|
de4431a559 | ||
|
|
928785493b | ||
|
|
df696ec9d3 | ||
|
|
0debaf1237 | ||
|
|
23f4ef0bd1 | ||
|
|
c8d9a6aed9 | ||
|
|
92fb2be27c | ||
|
|
3dc66d0a9a | ||
|
|
b10ee28bfe | ||
|
|
b3d25a9615 | ||
|
|
9392e513a0 | ||
|
|
57c55d7f3c | ||
|
|
7d04a0d1eb | ||
|
|
9b043c122f |
@@ -1 +0,0 @@
|
|||||||
ignores: ["*-loader", "webpack-cli", "@types/jest"]
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
build-cli
|
build-cli
|
||||||
webpack.cli.js
|
**/webpack**.config.js
|
||||||
webpack.main.js
|
|
||||||
webpack.renderer.js
|
|
||||||
|
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"root": true,
|
"root": true,
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
|
"webextensions": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
|
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": ["./tsconfig.eslint.json"],
|
"project": ["./tsconfig.json"],
|
||||||
"sourceType": "module",
|
"sourceType": "module",
|
||||||
"ecmaVersion": 2020
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
@@ -52,12 +53,12 @@
|
|||||||
"newlines-between": "always",
|
"newlines-between": "always",
|
||||||
"pathGroups": [
|
"pathGroups": [
|
||||||
{
|
{
|
||||||
"pattern": "@/jslib/**/*",
|
"pattern": "@bitwarden/**",
|
||||||
"group": "external",
|
"group": "external",
|
||||||
"position": "after"
|
"position": "after"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "@/src/**/*",
|
"pattern": "src/**/*",
|
||||||
"group": "parent",
|
"group": "parent",
|
||||||
"position": "before"
|
"position": "before"
|
||||||
}
|
}
|
||||||
@@ -85,11 +86,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.html"],
|
"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
28
.github/renovate.json
vendored
@@ -2,30 +2,40 @@
|
|||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:base",
|
"config:base",
|
||||||
"github>bitwarden/renovate-config:pin-actions",
|
|
||||||
":combinePatchMinorReleases",
|
":combinePatchMinorReleases",
|
||||||
":dependencyDashboard",
|
":dependencyDashboard",
|
||||||
":maintainLockFilesWeekly",
|
":maintainLockFilesWeekly",
|
||||||
":pinAllExceptPeerDependencies",
|
":pinAllExceptPeerDependencies",
|
||||||
":prConcurrentLimit10",
|
":prConcurrentLimit10",
|
||||||
":rebaseStalePrs",
|
":rebaseStalePrs",
|
||||||
":separateMajorReleases",
|
"schedule:weekends",
|
||||||
"group:monorepos",
|
":separateMajorReleases"
|
||||||
"schedule:weekends"
|
|
||||||
],
|
],
|
||||||
"enabledManagers": ["github-actions", "npm"],
|
"enabledManagers": ["github-actions", "npm"],
|
||||||
"commitMessagePrefix": "[deps]:",
|
|
||||||
"commitMessageTopic": "{{depName}}",
|
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"groupName": "gh minor",
|
||||||
|
"matchManagers": ["github-actions"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"groupName": "npm minor",
|
"groupName": "npm minor",
|
||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
"matchUpdateTypes": ["minor", "patch"]
|
"matchUpdateTypes": ["minor", "patch"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchFileNames": ["package.json"],
|
"packageNames": ["typescript"],
|
||||||
"description": "Admin Console owns general dependencies",
|
"matchUpdateTypes": ["major", "minor"],
|
||||||
"reviewers": ["team:team-admin-console-dev"]
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packageNames": ["typescript"],
|
||||||
|
"matchUpdateTypes": "patch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "jest",
|
||||||
|
"packageNames": ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
|
||||||
|
"matchUpdateTypes": "major"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -2,9 +2,14 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request: {}
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'l10n_master'
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/workflows/**'
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
name: CLOC
|
name: CLOC
|
||||||
@@ -44,7 +49,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
_PKG_FETCH_NODE_VERSION: 20.11.0
|
_PKG_FETCH_NODE_VERSION: 18.5.0
|
||||||
_PKG_FETCH_VERSION: 3.4
|
_PKG_FETCH_VERSION: 3.4
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -55,7 +60,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: '20.11.0'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
@@ -139,7 +144,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
_PKG_FETCH_NODE_VERSION: 20.11.0
|
_PKG_FETCH_NODE_VERSION: 18.5.0
|
||||||
_PKG_FETCH_VERSION: 3.4
|
_PKG_FETCH_VERSION: 3.4
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -150,7 +155,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: '20.11.0'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
@@ -226,7 +231,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
_WIN_PKG_FETCH_VERSION: 20.11.0
|
_WIN_PKG_FETCH_VERSION: 18.5.0
|
||||||
_WIN_PKG_VERSION: 3.4
|
_WIN_PKG_VERSION: 3.4
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
@@ -242,7 +247,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: '20.11.0'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
@@ -332,7 +337,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Zip
|
- name: Zip
|
||||||
shell: cmd
|
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
|
- name: Version Test
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -382,7 +388,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: '20.11.0'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
@@ -456,7 +462,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: '20.11.0'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
@@ -510,7 +516,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: '20.11.0'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Update NPM
|
- name: Update NPM
|
||||||
run: |
|
run: |
|
||||||
@@ -580,7 +586,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
CSC_FOR_PULL_REQUEST: true
|
|
||||||
|
|
||||||
- name: Upload .zip artifact
|
- name: Upload .zip artifact
|
||||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
@@ -625,7 +630,7 @@ jobs:
|
|||||||
- macos-gui
|
- macos-gui
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
SETUP_STATUS: ${{ needs.setup.result }}
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: main
|
branch: master
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
|||||||
54
.github/workflows/test.yml
vendored
54
.github/workflows/test.yml
vendored
@@ -1,54 +0,0 @@
|
|||||||
---
|
|
||||||
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@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
|
|
||||||
- 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@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
|
||||||
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
|
|
||||||
132
.github/workflows/version-bump.yml
vendored
132
.github/workflows/version-bump.yml
vendored
@@ -1,25 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
run-name: Version Bump - v${{ inputs.version_number }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version_number:
|
version_number:
|
||||||
description: "New version (example: '2024.1.0')"
|
description: "New Version"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
|
||||||
cut_rc_branch:
|
|
||||||
description: "Cut RC branch?"
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: "Bump Version to v${{ inputs.version_number }}"
|
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
@@ -29,15 +26,7 @@ jobs:
|
|||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-gpg-private-key,
|
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||||
github-gpg-private-key-passphrase,
|
|
||||||
github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- name: Checkout Branch
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
repository: bitwarden/directory-connector
|
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||||
@@ -48,74 +37,39 @@ jobs:
|
|||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- name: Create Version Branch
|
- name: Create Version Branch
|
||||||
id: create-branch
|
|
||||||
run: |
|
run: |
|
||||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
git switch -c $NAME
|
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Verify input version
|
- name: Checkout Version Branch
|
||||||
env:
|
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
with:
|
||||||
run: |
|
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
|
|
||||||
# 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: Bump Version - Package
|
- name: Bump Version - Package
|
||||||
uses: bitwarden/gh-actions/version-bump@main
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
version: ${{ github.event.inputs.version_number }}
|
||||||
file_path: "./package.json"
|
file_path: "./package.json"
|
||||||
|
|
||||||
- name: Setup git
|
- name: Commit files
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
|
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||||
- 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 ${{ inputs.version_number }}" -a
|
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
env:
|
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
|
||||||
run: git push -u origin $PR_BRANCH
|
|
||||||
|
|
||||||
- name: Create Version PR
|
- name: Create Version PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
|
||||||
id: create-pr
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
BASE_BRANCH: master
|
||||||
|
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||||
run: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
gh pr create --title "$TITLE" \
|
||||||
--base "main" \
|
--base "$BASE" \
|
||||||
--head "$PR_BRANCH" \
|
--head "$PR_BRANCH" \
|
||||||
--label "version update" \
|
--label "version update" \
|
||||||
--label "automated pr" \
|
--label "automated pr" \
|
||||||
@@ -128,42 +82,4 @@ jobs:
|
|||||||
- [X] Other
|
- [X] Other
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
Automated version bump to ${{ inputs.version_number }}")
|
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Approve PR
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
|
||||||
run: gh pr review $PR_NUMBER --approve
|
|
||||||
|
|
||||||
- name: Merge PR
|
|
||||||
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
|
|
||||||
needs: bump_version
|
|
||||||
if: ${{ inputs.cut_rc_branch == true }}
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout Branch
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Check if RC branch exists
|
|
||||||
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: Cut RC branch
|
|
||||||
run: |
|
|
||||||
git switch --quiet --create rc
|
|
||||||
git push --quiet --set-upstream origin rc
|
|
||||||
|
|||||||
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@@ -8,4 +8,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call-workflow:
|
call-workflow:
|
||||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@c970b0fb89bd966749280e832928db62040812bf
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Directive } from "@angular/core";
|
import { Directive } from "@angular/core";
|
||||||
|
|
||||||
|
import { ModalRef } from "./modal/modal.ref";
|
||||||
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.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.
|
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
|
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
|
||||||
|
|
||||||
|
import { ValidationService } from "../services/validation.service";
|
||||||
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
|
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.
|
* Provides error handling, in particular for any error returned by the server in an api call.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
|
||||||
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||||
@@ -7,7 +7,7 @@ import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|||||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuardService implements CanActivate {
|
export class AuthGuardService {
|
||||||
constructor(
|
constructor(
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
|
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 { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
||||||
import { AuditService as AuditServiceAbstraction } from "@/jslib/common/src/abstractions/audit.service";
|
import { AuditService as AuditServiceAbstraction } from "@/jslib/common/src/abstractions/audit.service";
|
||||||
@@ -11,6 +19,7 @@ import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/ab
|
|||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||||
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.service";
|
import { 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 { FileUploadService as FileUploadServiceAbstraction } from "@/jslib/common/src/abstractions/fileUpload.service";
|
||||||
import { FolderService as FolderServiceAbstraction } from "@/jslib/common/src/abstractions/folder.service";
|
import { FolderService as FolderServiceAbstraction } from "@/jslib/common/src/abstractions/folder.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
@@ -50,6 +59,7 @@ import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.servic
|
|||||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||||
import { EventService } from "@/jslib/common/src/services/event.service";
|
import { 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 { FileUploadService } from "@/jslib/common/src/services/fileUpload.service";
|
||||||
import { FolderService } from "@/jslib/common/src/services/folder.service";
|
import { FolderService } from "@/jslib/common/src/services/folder.service";
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
||||||
@@ -70,14 +80,8 @@ import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service"
|
|||||||
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
|
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
|
||||||
import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service";
|
import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service";
|
||||||
import { VaultTimeoutService } from "@/jslib/common/src/services/vaultTimeout.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({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
@@ -369,6 +373,16 @@ import { ValidationService } from "./validation.service";
|
|||||||
),
|
),
|
||||||
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ExportServiceAbstraction,
|
||||||
|
useClass: ExportService,
|
||||||
|
deps: [
|
||||||
|
FolderServiceAbstraction,
|
||||||
|
CipherServiceAbstraction,
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: SearchServiceAbstraction,
|
provide: SearchServiceAbstraction,
|
||||||
useClass: SearchService,
|
useClass: SearchService,
|
||||||
@@ -407,6 +421,11 @@ import { ValidationService } from "./validation.service";
|
|||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CryptoFunctionServiceAbstraction,
|
||||||
|
useClass: WebCryptoFunctionService,
|
||||||
|
deps: ["WINDOW"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: EventServiceAbstraction,
|
provide: EventServiceAbstraction,
|
||||||
useClass: EventService,
|
useClass: EventService,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { CanActivate, Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LockGuardService implements CanActivate {
|
export class LockGuardService {
|
||||||
protected homepage = "vault";
|
protected homepage = "vault";
|
||||||
protected loginpage = "login";
|
protected loginpage = "login";
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
|
|
||||||
|
|
||||||
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||||
|
|
||||||
import { ModalService } from "./modal.service";
|
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";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||||
* See UserVerificationService for any other situation where you need to verify the user's identity.
|
* See UserVerificationService for any other situation where you need to verify the user's identity.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { CanActivate, Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UnauthGuardService implements CanActivate {
|
export class UnauthGuardService {
|
||||||
protected homepage = "vault";
|
protected homepage = "vault";
|
||||||
constructor(
|
constructor(
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,599 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
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",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { ApiLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/apiLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
|
||||||
|
|
||||||
describe("ApiLogInStrategy", () => {
|
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
|
||||||
let apiService: SubstituteOf<ApiService>;
|
|
||||||
let tokenService: SubstituteOf<TokenService>;
|
|
||||||
let appIdService: SubstituteOf<AppIdService>;
|
|
||||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
|
||||||
let messagingService: SubstituteOf<MessagingService>;
|
|
||||||
let logService: SubstituteOf<LogService>;
|
|
||||||
let environmentService: SubstituteOf<EnvironmentService>;
|
|
||||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
|
||||||
let stateService: SubstituteOf<StateService>;
|
|
||||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
|
||||||
|
|
||||||
let apiLogInStrategy: ApiLogInStrategy;
|
|
||||||
let credentials: ApiLogInCredentials;
|
|
||||||
|
|
||||||
const deviceId = Utils.newGuid();
|
|
||||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
|
||||||
const apiClientId = "API_CLIENT_ID";
|
|
||||||
const apiClientSecret = "API_CLIENT_SECRET";
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
apiService = Substitute.for<ApiService>();
|
|
||||||
tokenService = Substitute.for<TokenService>();
|
|
||||||
appIdService = Substitute.for<AppIdService>();
|
|
||||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
|
||||||
messagingService = Substitute.for<MessagingService>();
|
|
||||||
logService = Substitute.for<LogService>();
|
|
||||||
environmentService = Substitute.for<EnvironmentService>();
|
|
||||||
stateService = Substitute.for<StateService>();
|
|
||||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
|
||||||
twoFactorService = Substitute.for<TwoFactorService>();
|
|
||||||
|
|
||||||
appIdService.getAppId().resolves(deviceId);
|
|
||||||
tokenService.getTwoFactorToken().resolves(null);
|
|
||||||
|
|
||||||
apiLogInStrategy = new ApiLogInStrategy(
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
environmentService,
|
|
||||||
keyConnectorService
|
|
||||||
);
|
|
||||||
|
|
||||||
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends api key credentials to the server", async () => {
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
|
||||||
Arg.is((actual) => {
|
|
||||||
const apiTokenRequest = actual as any;
|
|
||||||
return (
|
|
||||||
apiTokenRequest.clientId === apiClientId &&
|
|
||||||
apiTokenRequest.clientSecret === apiClientSecret &&
|
|
||||||
apiTokenRequest.device.identifier === deviceId &&
|
|
||||||
apiTokenRequest.twoFactor.provider == null &&
|
|
||||||
apiTokenRequest.twoFactor.token == null &&
|
|
||||||
apiTokenRequest.captchaResponse == null
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the local environment after a successful login", async () => {
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
stateService.received(1).setApiKeyClientId(apiClientId);
|
|
||||||
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
|
|
||||||
stateService.received(1).addAccount(Arg.any());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets and sets the Key Connector key from environmentUrl", async () => {
|
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
|
||||||
tokenResponse.apiUseKeyConnector = true;
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
|
|
||||||
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
|
|
||||||
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { Account, AccountProfile, AccountTokens } from "@/jslib/common/src/models/domain/account";
|
|
||||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
|
||||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
|
||||||
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
import { PasswordTokenRequest } from "@/jslib/common/src/models/request/identityToken/passwordTokenRequest";
|
|
||||||
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
|
|
||||||
import { IdentityCaptchaResponse } from "@/jslib/common/src/models/response/identityCaptchaResponse";
|
|
||||||
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
|
||||||
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
|
|
||||||
|
|
||||||
const email = "hello@world.com";
|
|
||||||
const masterPassword = "password";
|
|
||||||
|
|
||||||
const deviceId = Utils.newGuid();
|
|
||||||
const accessToken = "ACCESS_TOKEN";
|
|
||||||
const refreshToken = "REFRESH_TOKEN";
|
|
||||||
const encKey = "ENC_KEY";
|
|
||||||
const privateKey = "PRIVATE_KEY";
|
|
||||||
const captchaSiteKey = "CAPTCHA_SITE_KEY";
|
|
||||||
const kdf = 0;
|
|
||||||
const kdfIterations = 10000;
|
|
||||||
const userId = Utils.newGuid();
|
|
||||||
const masterPasswordHash = "MASTER_PASSWORD_HASH";
|
|
||||||
|
|
||||||
const decodedToken = {
|
|
||||||
sub: userId,
|
|
||||||
email: email,
|
|
||||||
premium: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const twoFactorProviderType = TwoFactorProviderType.Authenticator;
|
|
||||||
const twoFactorToken = "TWO_FACTOR_TOKEN";
|
|
||||||
const twoFactorRemember = true;
|
|
||||||
|
|
||||||
export function identityTokenResponseFactory() {
|
|
||||||
return new IdentityTokenResponse({
|
|
||||||
ForcePasswordReset: false,
|
|
||||||
Kdf: kdf,
|
|
||||||
KdfIterations: kdfIterations,
|
|
||||||
Key: encKey,
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
ResetMasterPassword: false,
|
|
||||||
access_token: accessToken,
|
|
||||||
expires_in: 3600,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
scope: "api offline_access",
|
|
||||||
token_type: "Bearer",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("LogInStrategy", () => {
|
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
|
||||||
let apiService: SubstituteOf<ApiService>;
|
|
||||||
let tokenService: SubstituteOf<TokenService>;
|
|
||||||
let appIdService: SubstituteOf<AppIdService>;
|
|
||||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
|
||||||
let messagingService: SubstituteOf<MessagingService>;
|
|
||||||
let logService: SubstituteOf<LogService>;
|
|
||||||
let stateService: SubstituteOf<StateService>;
|
|
||||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
|
||||||
let authService: SubstituteOf<AuthService>;
|
|
||||||
|
|
||||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
|
||||||
let credentials: PasswordLogInCredentials;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
apiService = Substitute.for<ApiService>();
|
|
||||||
tokenService = Substitute.for<TokenService>();
|
|
||||||
appIdService = Substitute.for<AppIdService>();
|
|
||||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
|
||||||
messagingService = Substitute.for<MessagingService>();
|
|
||||||
logService = Substitute.for<LogService>();
|
|
||||||
stateService = Substitute.for<StateService>();
|
|
||||||
twoFactorService = Substitute.for<TwoFactorService>();
|
|
||||||
authService = Substitute.for<AuthService>();
|
|
||||||
|
|
||||||
appIdService.getAppId().resolves(deviceId);
|
|
||||||
|
|
||||||
// The base class is abstract so we test it via PasswordLogInStrategy
|
|
||||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
authService
|
|
||||||
);
|
|
||||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("base class", () => {
|
|
||||||
it("sets the local environment after a successful login", async () => {
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
tokenService.decodeToken(accessToken).resolves(decodedToken);
|
|
||||||
|
|
||||||
await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
stateService.received(1).addAccount(
|
|
||||||
new Account({
|
|
||||||
profile: {
|
|
||||||
...new AccountProfile(),
|
|
||||||
...{
|
|
||||||
userId: userId,
|
|
||||||
email: email,
|
|
||||||
hasPremiumPersonally: false,
|
|
||||||
kdfIterations: kdfIterations,
|
|
||||||
kdfType: kdf,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tokens: {
|
|
||||||
...new AccountTokens(),
|
|
||||||
...{
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
cryptoService.received(1).setEncKey(encKey);
|
|
||||||
cryptoService.received(1).setEncPrivateKey(privateKey);
|
|
||||||
|
|
||||||
stateService.received(1).setBiometricLocked(false);
|
|
||||||
messagingService.received(1).send("loggedIn");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds AuthResult", async () => {
|
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
|
||||||
tokenResponse.forcePasswordReset = true;
|
|
||||||
tokenResponse.resetMasterPassword = true;
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
const result = await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
const expected = new AuthResult();
|
|
||||||
expected.forcePasswordReset = true;
|
|
||||||
expected.resetMasterPassword = true;
|
|
||||||
expected.twoFactorProviders = null;
|
|
||||||
expected.captchaSiteKey = "";
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects login if CAPTCHA is required", async () => {
|
|
||||||
// Sample CAPTCHA response
|
|
||||||
const tokenResponse = new IdentityCaptchaResponse({
|
|
||||||
error: "invalid_grant",
|
|
||||||
error_description: "Captcha required.",
|
|
||||||
HCaptcha_SiteKey: captchaSiteKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
const result = await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
stateService.didNotReceive().addAccount(Arg.any());
|
|
||||||
messagingService.didNotReceive().send(Arg.any());
|
|
||||||
|
|
||||||
const expected = new AuthResult();
|
|
||||||
expected.captchaSiteKey = captchaSiteKey;
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("makes a new public and private key for an old account", async () => {
|
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
|
||||||
tokenResponse.privateKey = null;
|
|
||||||
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
apiService.received(1).postAccountKeys(Arg.any());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Two-factor authentication", () => {
|
|
||||||
it("rejects login if 2FA is required", async () => {
|
|
||||||
// Sample response where TOTP 2FA required
|
|
||||||
const tokenResponse = new IdentityTwoFactorResponse({
|
|
||||||
TwoFactorProviders: ["0"],
|
|
||||||
TwoFactorProviders2: { 0: null },
|
|
||||||
error: "invalid_grant",
|
|
||||||
error_description: "Two factor required.",
|
|
||||||
});
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
const result = await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
stateService.didNotReceive().addAccount(Arg.any());
|
|
||||||
messagingService.didNotReceive().send(Arg.any());
|
|
||||||
|
|
||||||
const expected = new AuthResult();
|
|
||||||
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
|
||||||
expected.twoFactorProviders.set(0, null);
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends stored 2FA token to server", async () => {
|
|
||||||
tokenService.getTwoFactorToken().resolves(twoFactorToken);
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
|
|
||||||
await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
|
||||||
Arg.is((actual) => {
|
|
||||||
const passwordTokenRequest = actual as any;
|
|
||||||
return (
|
|
||||||
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
|
|
||||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
|
||||||
passwordTokenRequest.twoFactor.remember === false
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends 2FA token provided by user to server (single step)", async () => {
|
|
||||||
// This occurs if the user enters the 2FA code as an argument in the CLI
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
credentials.twoFactor = new TokenRequestTwoFactor(
|
|
||||||
twoFactorProviderType,
|
|
||||||
twoFactorToken,
|
|
||||||
twoFactorRemember
|
|
||||||
);
|
|
||||||
|
|
||||||
await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
|
||||||
Arg.is((actual) => {
|
|
||||||
const passwordTokenRequest = actual as any;
|
|
||||||
return (
|
|
||||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
|
||||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
|
||||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends 2FA token provided by user to server (two-step)", async () => {
|
|
||||||
// Simulate a partially completed login
|
|
||||||
passwordLogInStrategy.tokenRequest = new PasswordTokenRequest(
|
|
||||||
email,
|
|
||||||
masterPasswordHash,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
|
|
||||||
await passwordLogInStrategy.logInTwoFactor(
|
|
||||||
new TokenRequestTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
|
||||||
Arg.is((actual) => {
|
|
||||||
const passwordTokenRequest = actual as any;
|
|
||||||
return (
|
|
||||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
|
||||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
|
||||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { HashPurpose } from "@/jslib/common/src/enums/hashPurpose";
|
|
||||||
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
|
||||||
|
|
||||||
const email = "hello@world.com";
|
|
||||||
const masterPassword = "password";
|
|
||||||
const hashedPassword = "HASHED_PASSWORD";
|
|
||||||
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
|
|
||||||
const preloginKey = new SymmetricCryptoKey(
|
|
||||||
Utils.fromB64ToArray(
|
|
||||||
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const deviceId = Utils.newGuid();
|
|
||||||
|
|
||||||
describe("PasswordLogInStrategy", () => {
|
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
|
||||||
let apiService: SubstituteOf<ApiService>;
|
|
||||||
let tokenService: SubstituteOf<TokenService>;
|
|
||||||
let appIdService: SubstituteOf<AppIdService>;
|
|
||||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
|
||||||
let messagingService: SubstituteOf<MessagingService>;
|
|
||||||
let logService: SubstituteOf<LogService>;
|
|
||||||
let stateService: SubstituteOf<StateService>;
|
|
||||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
|
||||||
let authService: SubstituteOf<AuthService>;
|
|
||||||
|
|
||||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
|
||||||
let credentials: PasswordLogInCredentials;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
apiService = Substitute.for<ApiService>();
|
|
||||||
tokenService = Substitute.for<TokenService>();
|
|
||||||
appIdService = Substitute.for<AppIdService>();
|
|
||||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
|
||||||
messagingService = Substitute.for<MessagingService>();
|
|
||||||
logService = Substitute.for<LogService>();
|
|
||||||
stateService = Substitute.for<StateService>();
|
|
||||||
twoFactorService = Substitute.for<TwoFactorService>();
|
|
||||||
authService = Substitute.for<AuthService>();
|
|
||||||
|
|
||||||
appIdService.getAppId().resolves(deviceId);
|
|
||||||
tokenService.getTwoFactorToken().resolves(null);
|
|
||||||
|
|
||||||
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
|
|
||||||
|
|
||||||
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
|
|
||||||
cryptoService
|
|
||||||
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
|
|
||||||
.resolves(localHashedPassword);
|
|
||||||
|
|
||||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
authService
|
|
||||||
);
|
|
||||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends master password credentials to the server", async () => {
|
|
||||||
await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
|
||||||
Arg.is((actual) => {
|
|
||||||
const passwordTokenRequest = actual as any; // Need to access private fields
|
|
||||||
return (
|
|
||||||
passwordTokenRequest.email === email &&
|
|
||||||
passwordTokenRequest.masterPasswordHash === hashedPassword &&
|
|
||||||
passwordTokenRequest.device.identifier === deviceId &&
|
|
||||||
passwordTokenRequest.twoFactor.provider == null &&
|
|
||||||
passwordTokenRequest.twoFactor.token == null &&
|
|
||||||
passwordTokenRequest.captchaResponse == null
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the local environment after a successful login", async () => {
|
|
||||||
await passwordLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
cryptoService.received(1).setKey(preloginKey);
|
|
||||||
cryptoService.received(1).setKeyHash(localHashedPassword);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { SsoLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/ssoLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { SsoLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
|
||||||
|
|
||||||
describe("SsoLogInStrategy", () => {
|
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
|
||||||
let apiService: SubstituteOf<ApiService>;
|
|
||||||
let tokenService: SubstituteOf<TokenService>;
|
|
||||||
let appIdService: SubstituteOf<AppIdService>;
|
|
||||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
|
||||||
let messagingService: SubstituteOf<MessagingService>;
|
|
||||||
let logService: SubstituteOf<LogService>;
|
|
||||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
|
||||||
let stateService: SubstituteOf<StateService>;
|
|
||||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
|
||||||
|
|
||||||
let ssoLogInStrategy: SsoLogInStrategy;
|
|
||||||
let credentials: SsoLogInCredentials;
|
|
||||||
|
|
||||||
const deviceId = Utils.newGuid();
|
|
||||||
const encKey = "ENC_KEY";
|
|
||||||
const privateKey = "PRIVATE_KEY";
|
|
||||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
|
||||||
|
|
||||||
const ssoCode = "SSO_CODE";
|
|
||||||
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
|
|
||||||
const ssoRedirectUrl = "SSO_REDIRECT_URL";
|
|
||||||
const ssoOrgId = "SSO_ORG_ID";
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
apiService = Substitute.for<ApiService>();
|
|
||||||
tokenService = Substitute.for<TokenService>();
|
|
||||||
appIdService = Substitute.for<AppIdService>();
|
|
||||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
|
||||||
messagingService = Substitute.for<MessagingService>();
|
|
||||||
logService = Substitute.for<LogService>();
|
|
||||||
stateService = Substitute.for<StateService>();
|
|
||||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
|
||||||
twoFactorService = Substitute.for<TwoFactorService>();
|
|
||||||
|
|
||||||
tokenService.getTwoFactorToken().resolves(null);
|
|
||||||
appIdService.getAppId().resolves(deviceId);
|
|
||||||
|
|
||||||
ssoLogInStrategy = new SsoLogInStrategy(
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
keyConnectorService
|
|
||||||
);
|
|
||||||
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends SSO information to server", async () => {
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
|
||||||
|
|
||||||
await ssoLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
apiService.received(1).postIdentityToken(
|
|
||||||
Arg.is((actual) => {
|
|
||||||
const ssoTokenRequest = actual as any;
|
|
||||||
return (
|
|
||||||
ssoTokenRequest.code === ssoCode &&
|
|
||||||
ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
|
|
||||||
ssoTokenRequest.redirectUri === ssoRedirectUrl &&
|
|
||||||
ssoTokenRequest.device.identifier === deviceId &&
|
|
||||||
ssoTokenRequest.twoFactor.provider == null &&
|
|
||||||
ssoTokenRequest.twoFactor.token == null
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not set keys for new SSO user flow", async () => {
|
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
|
||||||
tokenResponse.key = null;
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
await ssoLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
|
|
||||||
cryptoService.didNotReceive().setEncKey(encKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets and sets KeyConnector key for enrolled user", async () => {
|
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
|
||||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
await ssoLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("converts new SSO user to Key Connector on first login", async () => {
|
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
|
||||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
|
||||||
tokenResponse.key = null;
|
|
||||||
|
|
||||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
|
||||||
|
|
||||||
await ssoLogInStrategy.logIn(credentials);
|
|
||||||
|
|
||||||
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { FileUploadService } from "@/jslib/common/src/abstractions/fileUpload.service";
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { SearchService } from "@/jslib/common/src/abstractions/search.service";
|
|
||||||
import { SettingsService } from "@/jslib/common/src/abstractions/settings.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { Cipher } from "@/jslib/common/src/models/domain/cipher";
|
|
||||||
import { EncArrayBuffer } from "@/jslib/common/src/models/domain/encArrayBuffer";
|
|
||||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
|
||||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
|
||||||
import { CipherService } from "@/jslib/common/src/services/cipher.service";
|
|
||||||
|
|
||||||
const ENCRYPTED_TEXT = "This data has been encrypted";
|
|
||||||
const ENCRYPTED_BYTES = new EncArrayBuffer(Utils.fromUtf8ToArray(ENCRYPTED_TEXT).buffer);
|
|
||||||
|
|
||||||
describe("Cipher Service", () => {
|
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
|
||||||
let stateService: SubstituteOf<StateService>;
|
|
||||||
let settingsService: SubstituteOf<SettingsService>;
|
|
||||||
let apiService: SubstituteOf<ApiService>;
|
|
||||||
let fileUploadService: SubstituteOf<FileUploadService>;
|
|
||||||
let i18nService: SubstituteOf<I18nService>;
|
|
||||||
let searchService: SubstituteOf<SearchService>;
|
|
||||||
let logService: SubstituteOf<LogService>;
|
|
||||||
|
|
||||||
let cipherService: CipherService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
stateService = Substitute.for<StateService>();
|
|
||||||
settingsService = Substitute.for<SettingsService>();
|
|
||||||
apiService = Substitute.for<ApiService>();
|
|
||||||
fileUploadService = Substitute.for<FileUploadService>();
|
|
||||||
i18nService = Substitute.for<I18nService>();
|
|
||||||
searchService = Substitute.for<SearchService>();
|
|
||||||
logService = Substitute.for<LogService>();
|
|
||||||
|
|
||||||
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
|
|
||||||
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT));
|
|
||||||
|
|
||||||
cipherService = new CipherService(
|
|
||||||
cryptoService,
|
|
||||||
settingsService,
|
|
||||||
apiService,
|
|
||||||
fileUploadService,
|
|
||||||
i18nService,
|
|
||||||
() => searchService,
|
|
||||||
logService,
|
|
||||||
stateService
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("attachments upload encrypted file contents", async () => {
|
|
||||||
const fileName = "filename";
|
|
||||||
const fileData = new Uint8Array(10).buffer;
|
|
||||||
cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer));
|
|
||||||
|
|
||||||
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
|
|
||||||
|
|
||||||
fileUploadService
|
|
||||||
.received(1)
|
|
||||||
.uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { webcrypto } from "crypto";
|
|
||||||
|
|
||||||
Object.defineProperty(window, "crypto", {
|
|
||||||
value: webcrypto,
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
11
jslib/common/src/abstractions/export.service.ts
Normal file
11
jslib/common/src/abstractions/export.service.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
19
jslib/common/src/abstractions/import.service.ts
Normal file
19
jslib/common/src/abstractions/import.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
74
jslib/common/src/enums/importOptions.ts
Normal file
74
jslib/common/src/enums/importOptions.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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"];
|
||||||
59
jslib/common/src/importers/ascendoCsvImporter.ts
Normal file
59
jslib/common/src/importers/ascendoCsvImporter.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
jslib/common/src/importers/avastCsvImporter.ts
Normal file
28
jslib/common/src/importers/avastCsvImporter.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
jslib/common/src/importers/avastJsonImporter.ts
Normal file
68
jslib/common/src/importers/avastJsonImporter.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
jslib/common/src/importers/aviraCsvImporter.ts
Normal file
41
jslib/common/src/importers/aviraCsvImporter.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
466
jslib/common/src/importers/baseImporter.ts
Normal file
466
jslib/common/src/importers/baseImporter.ts
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
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(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
jslib/common/src/importers/bitwardenCsvImporter.ts
Normal file
120
jslib/common/src/importers/bitwardenCsvImporter.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
179
jslib/common/src/importers/bitwardenJsonImporter.ts
Normal file
179
jslib/common/src/importers/bitwardenJsonImporter.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
jslib/common/src/importers/blackBerryCsvImporter.ts
Normal file
36
jslib/common/src/importers/blackBerryCsvImporter.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
jslib/common/src/importers/blurCsvImporter.ts
Normal file
41
jslib/common/src/importers/blurCsvImporter.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
jslib/common/src/importers/buttercupCsvImporter.ts
Normal file
50
jslib/common/src/importers/buttercupCsvImporter.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
jslib/common/src/importers/chromeCsvImporter.ts
Normal file
28
jslib/common/src/importers/chromeCsvImporter.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
jslib/common/src/importers/clipperzHtmlImporter.ts
Normal file
88
jslib/common/src/importers/clipperzHtmlImporter.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
jslib/common/src/importers/codebookCsvImporter.ts
Normal file
47
jslib/common/src/importers/codebookCsvImporter.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
60
jslib/common/src/importers/encryptrCsvImporter.ts
Normal file
60
jslib/common/src/importers/encryptrCsvImporter.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
jslib/common/src/importers/enpassCsvImporter.ts
Normal file
133
jslib/common/src/importers/enpassCsvImporter.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
jslib/common/src/importers/enpassJsonImporter.ts
Normal file
191
jslib/common/src/importers/enpassJsonImporter.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { CipherType } from "../enums/cipherType";
|
||||||
|
import { FieldType } from "../enums/fieldType";
|
||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { CardView } from "../models/view/cardView";
|
||||||
|
import { CipherView } from "../models/view/cipherView";
|
||||||
|
import { FolderView } from "../models/view/folderView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results = JSON.parse(data);
|
||||||
|
if (results == null || results.items == null || results.items.length === 0) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foldersMap = new Map<string, string>();
|
||||||
|
const foldersIndexMap = new Map<string, number>();
|
||||||
|
const folderTree = this.buildFolderTree(results.folders);
|
||||||
|
this.flattenFolderTree(null, folderTree, foldersMap);
|
||||||
|
foldersMap.forEach((val, key) => {
|
||||||
|
foldersIndexMap.set(key, result.folders.length);
|
||||||
|
const f = new FolderView();
|
||||||
|
f.name = val;
|
||||||
|
result.folders.push(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
results.items.forEach((item: any) => {
|
||||||
|
if (item.folders != null && item.folders.length > 0 && foldersIndexMap.has(item.folders[0])) {
|
||||||
|
result.folderRelationships.push([
|
||||||
|
result.ciphers.length,
|
||||||
|
foldersIndexMap.get(item.folders[0]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(item.title);
|
||||||
|
cipher.favorite = item.favorite > 0;
|
||||||
|
|
||||||
|
if (item.template_type != null && item.fields != null && item.fields.length > 0) {
|
||||||
|
if (
|
||||||
|
item.template_type.indexOf("login.") === 0 ||
|
||||||
|
item.template_type.indexOf("password.") === 0
|
||||||
|
) {
|
||||||
|
this.processLogin(cipher, item.fields);
|
||||||
|
} else if (item.template_type.indexOf("creditcard.") === 0) {
|
||||||
|
this.processCard(cipher, item.fields);
|
||||||
|
} else if (
|
||||||
|
item.template_type.indexOf("identity.") < 0 &&
|
||||||
|
item.fields.some((f: any) => f.type === "password" && !this.isNullOrWhitespace(f.value))
|
||||||
|
) {
|
||||||
|
this.processLogin(cipher, item.fields);
|
||||||
|
} else {
|
||||||
|
this.processNote(cipher, item.fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.notes += "\n" + this.getValueOrDefault(item.note, "");
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processLogin(cipher: CipherView, fields: any[]) {
|
||||||
|
const urls: string[] = [];
|
||||||
|
fields.forEach((field: any) => {
|
||||||
|
if (this.isNullOrWhitespace(field.value) || field.type === "section") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(field.type === "username" || field.type === "email") &&
|
||||||
|
this.isNullOrWhitespace(cipher.login.username)
|
||||||
|
) {
|
||||||
|
cipher.login.username = field.value;
|
||||||
|
} else if (field.type === "password" && this.isNullOrWhitespace(cipher.login.password)) {
|
||||||
|
cipher.login.password = field.value;
|
||||||
|
} else if (field.type === "totp" && this.isNullOrWhitespace(cipher.login.totp)) {
|
||||||
|
cipher.login.totp = field.value;
|
||||||
|
} else if (field.type === "url") {
|
||||||
|
urls.push(field.value);
|
||||||
|
} else {
|
||||||
|
this.processKvp(
|
||||||
|
cipher,
|
||||||
|
field.label,
|
||||||
|
field.value,
|
||||||
|
field.sensitive === 1 ? FieldType.Hidden : FieldType.Text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cipher.login.uris = this.makeUriArray(urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processCard(cipher: CipherView, fields: any[]) {
|
||||||
|
cipher.card = new CardView();
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
fields.forEach((field: any) => {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(field.value) ||
|
||||||
|
field.type === "section" ||
|
||||||
|
field.type === "ccType"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === "ccName" && this.isNullOrWhitespace(cipher.card.cardholderName)) {
|
||||||
|
cipher.card.cardholderName = field.value;
|
||||||
|
} else if (field.type === "ccNumber" && this.isNullOrWhitespace(cipher.card.number)) {
|
||||||
|
cipher.card.number = field.value;
|
||||||
|
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||||
|
} else if (field.type === "ccCvc" && this.isNullOrWhitespace(cipher.card.code)) {
|
||||||
|
cipher.card.code = field.value;
|
||||||
|
} else if (field.type === "ccExpiry" && this.isNullOrWhitespace(cipher.card.expYear)) {
|
||||||
|
if (!this.setCardExpiration(cipher, field.value)) {
|
||||||
|
this.processKvp(
|
||||||
|
cipher,
|
||||||
|
field.label,
|
||||||
|
field.value,
|
||||||
|
field.sensitive === 1 ? FieldType.Hidden : FieldType.Text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.processKvp(
|
||||||
|
cipher,
|
||||||
|
field.label,
|
||||||
|
field.value,
|
||||||
|
field.sensitive === 1 ? FieldType.Hidden : FieldType.Text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processNote(cipher: CipherView, fields: any[]) {
|
||||||
|
fields.forEach((field: any) => {
|
||||||
|
if (this.isNullOrWhitespace(field.value) || field.type === "section") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processKvp(
|
||||||
|
cipher,
|
||||||
|
field.label,
|
||||||
|
field.value,
|
||||||
|
field.sensitive === 1 ? FieldType.Hidden : FieldType.Text
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildFolderTree(folders: any[]): any[] {
|
||||||
|
if (folders == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const folderTree: any[] = [];
|
||||||
|
const map = new Map<string, any>([]);
|
||||||
|
folders.forEach((obj: any) => {
|
||||||
|
map.set(obj.uuid, obj);
|
||||||
|
obj.children = [];
|
||||||
|
});
|
||||||
|
folders.forEach((obj: any) => {
|
||||||
|
if (obj.parent_uuid != null && obj.parent_uuid !== "" && map.has(obj.parent_uuid)) {
|
||||||
|
map.get(obj.parent_uuid).children.push(obj);
|
||||||
|
} else {
|
||||||
|
folderTree.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return folderTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private flattenFolderTree(titlePrefix: string, tree: any[], map: Map<string, string>) {
|
||||||
|
if (tree == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tree.forEach((f: any) => {
|
||||||
|
if (f.title != null && f.title.trim() !== "") {
|
||||||
|
let title = f.title.trim();
|
||||||
|
if (titlePrefix != null && titlePrefix.trim() !== "") {
|
||||||
|
title = titlePrefix + "/" + title;
|
||||||
|
}
|
||||||
|
map.set(f.uuid, title);
|
||||||
|
if (f.children != null && f.children.length !== 0) {
|
||||||
|
this.flattenFolderTree(title, f.children, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
33
jslib/common/src/importers/firefoxCsvImporter.ts
Normal file
33
jslib/common/src/importers/firefoxCsvImporter.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class FirefoxCsvImporter 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
|
||||||
|
.filter((value) => {
|
||||||
|
return value.url !== "chrome://FirefoxAccounts";
|
||||||
|
})
|
||||||
|
.forEach((value) => {
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
const url = this.getValueOrDefault(value.url, this.getValueOrDefault(value.hostname));
|
||||||
|
cipher.name = this.getValueOrDefault(this.nameFromUrl(url), "--");
|
||||||
|
cipher.login.username = this.getValueOrDefault(value.username);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value.password);
|
||||||
|
cipher.login.uris = this.makeUriArray(url);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
jslib/common/src/importers/fsecureFskImporter.ts
Normal file
59
jslib/common/src/importers/fsecureFskImporter.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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 FSecureFskImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results = JSON.parse(data);
|
||||||
|
if (results == null || results.data == null) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in results.data) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (!results.data.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = results.data[key];
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(value.service);
|
||||||
|
cipher.notes = this.getValueOrDefault(value.notes);
|
||||||
|
|
||||||
|
if (value.style === "website" || value.style === "globe") {
|
||||||
|
cipher.login.username = this.getValueOrDefault(value.username);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value.password);
|
||||||
|
cipher.login.uris = this.makeUriArray(value.url);
|
||||||
|
} else if (value.style === "creditcard") {
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = new CardView();
|
||||||
|
cipher.card.cardholderName = this.getValueOrDefault(value.username);
|
||||||
|
cipher.card.number = this.getValueOrDefault(value.creditNumber);
|
||||||
|
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||||
|
cipher.card.code = this.getValueOrDefault(value.creditCvv);
|
||||||
|
if (!this.isNullOrWhitespace(value.creditExpiry)) {
|
||||||
|
if (!this.setCardExpiration(cipher, value.creditExpiry)) {
|
||||||
|
this.processKvp(cipher, "Expiration", value.creditExpiry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.isNullOrWhitespace(value.password)) {
|
||||||
|
this.processKvp(cipher, "PIN", value.password);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
jslib/common/src/importers/gnomeJsonImporter.ts
Normal file
71
jslib/common/src/importers/gnomeJsonImporter.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class GnomeJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results = JSON.parse(data);
|
||||||
|
if (results == null || Object.keys(results).length === 0) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const keyRing in results) {
|
||||||
|
if (
|
||||||
|
!results.hasOwnProperty(keyRing) || // eslint-disable-line
|
||||||
|
this.isNullOrWhitespace(keyRing) ||
|
||||||
|
results[keyRing].length === 0
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results[keyRing].forEach((value: any) => {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(value.display_name) ||
|
||||||
|
value.display_name.indexOf("http") !== 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processFolder(result, keyRing);
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = value.display_name.replace("http://", "").replace("https://", "");
|
||||||
|
if (cipher.name.length > 30) {
|
||||||
|
cipher.name = cipher.name.substring(0, 30);
|
||||||
|
}
|
||||||
|
cipher.login.password = this.getValueOrDefault(value.secret);
|
||||||
|
cipher.login.uris = this.makeUriArray(value.display_name);
|
||||||
|
|
||||||
|
if (value.attributes != null) {
|
||||||
|
cipher.login.username =
|
||||||
|
value.attributes != null
|
||||||
|
? this.getValueOrDefault(value.attributes.username_value)
|
||||||
|
: null;
|
||||||
|
for (const attr in value.attributes) {
|
||||||
|
if (
|
||||||
|
!value.attributes.hasOwnProperty(attr) || // eslint-disable-line
|
||||||
|
attr === "username_value" ||
|
||||||
|
attr === "xdg:schema"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.processKvp(cipher, attr, value.attributes[attr]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
jslib/common/src/importers/importError.ts
Normal file
5
jslib/common/src/importers/importError.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export class ImportError extends Error {
|
||||||
|
constructor(message?: string, public passwordRequired: boolean = false) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
jslib/common/src/importers/importer.ts
Normal file
6
jslib/common/src/importers/importer.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
export interface Importer {
|
||||||
|
organizationId: string;
|
||||||
|
parse(data: string): Promise<ImportResult>;
|
||||||
|
}
|
||||||
124
jslib/common/src/importers/kasperskyTxtImporter.ts
Normal file
124
jslib/common/src/importers/kasperskyTxtImporter.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
const NotesHeader = "Notes\n\n";
|
||||||
|
const ApplicationsHeader = "Applications\n\n";
|
||||||
|
const WebsitesHeader = "Websites\n\n";
|
||||||
|
const Delimiter = "\n---\n";
|
||||||
|
|
||||||
|
export class KasperskyTxtImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
|
||||||
|
let notesData: string;
|
||||||
|
let applicationsData: string;
|
||||||
|
let websitesData: string;
|
||||||
|
let workingData = this.splitNewLine(data).join("\n");
|
||||||
|
|
||||||
|
if (workingData.indexOf(NotesHeader) !== -1) {
|
||||||
|
const parts = workingData.split(NotesHeader);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
workingData = parts[0];
|
||||||
|
notesData = parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (workingData.indexOf(ApplicationsHeader) !== -1) {
|
||||||
|
const parts = workingData.split(ApplicationsHeader);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
workingData = parts[0];
|
||||||
|
applicationsData = parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (workingData.indexOf(WebsitesHeader) === 0) {
|
||||||
|
const parts = workingData.split(WebsitesHeader);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
workingData = parts[0];
|
||||||
|
websitesData = parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = this.parseDataCategory(notesData);
|
||||||
|
const applications = this.parseDataCategory(applicationsData);
|
||||||
|
const websites = this.parseDataCategory(websitesData);
|
||||||
|
|
||||||
|
notes.forEach((n) => {
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(n.get("Name"));
|
||||||
|
cipher.notes = this.getValueOrDefault(n.get("Text"));
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
websites.concat(applications).forEach((w) => {
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
const nameKey = w.has("Website name") ? "Website name" : "Application";
|
||||||
|
cipher.name = this.getValueOrDefault(w.get(nameKey), "");
|
||||||
|
if (!this.isNullOrWhitespace(w.get("Login name"))) {
|
||||||
|
if (!this.isNullOrWhitespace(cipher.name)) {
|
||||||
|
cipher.name += ": ";
|
||||||
|
}
|
||||||
|
cipher.name += w.get("Login name");
|
||||||
|
}
|
||||||
|
cipher.notes = this.getValueOrDefault(w.get("Comment"));
|
||||||
|
if (w.has("Website URL")) {
|
||||||
|
cipher.login.uris = this.makeUriArray(w.get("Website URL"));
|
||||||
|
}
|
||||||
|
cipher.login.username = this.getValueOrDefault(w.get("Login"));
|
||||||
|
cipher.login.password = this.getValueOrDefault(w.get("Password"));
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDataCategory(data: string): Map<string, string>[] {
|
||||||
|
if (this.isNullOrWhitespace(data) || data.indexOf(Delimiter) === -1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const items: Map<string, string>[] = [];
|
||||||
|
data.split(Delimiter).forEach((p) => {
|
||||||
|
if (p.indexOf("\n") === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = new Map<string, string>();
|
||||||
|
let itemComment: string;
|
||||||
|
let itemCommentKey: string;
|
||||||
|
p.split("\n").forEach((l) => {
|
||||||
|
if (itemComment != null) {
|
||||||
|
itemComment += "\n" + l;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const colonIndex = l.indexOf(":");
|
||||||
|
let key: string;
|
||||||
|
let val: string;
|
||||||
|
if (colonIndex === -1) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
key = l.substring(0, colonIndex);
|
||||||
|
if (l.length > colonIndex + 1) {
|
||||||
|
val = l.substring(colonIndex + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
item.set(key, val);
|
||||||
|
}
|
||||||
|
if (key === "Comment" || key === "Text") {
|
||||||
|
itemComment = val;
|
||||||
|
itemCommentKey = key;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (itemComment != null && itemCommentKey != null) {
|
||||||
|
item.set(itemCommentKey, itemComment);
|
||||||
|
}
|
||||||
|
if (item.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
jslib/common/src/importers/keepass2XmlImporter.ts
Normal file
101
jslib/common/src/importers/keepass2XmlImporter.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { FieldType } from "../enums/fieldType";
|
||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { FolderView } from "../models/view/folderView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class KeePass2XmlImporter extends BaseImporter implements Importer {
|
||||||
|
result = new ImportResult();
|
||||||
|
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const doc = this.parseXml(data);
|
||||||
|
if (doc == null) {
|
||||||
|
this.result.success = false;
|
||||||
|
return Promise.resolve(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootGroup = doc.querySelector("KeePassFile > Root > Group");
|
||||||
|
if (rootGroup == null) {
|
||||||
|
this.result.errorMessage = "Missing `KeePassFile > Root > Group` node.";
|
||||||
|
this.result.success = false;
|
||||||
|
return Promise.resolve(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.traverse(rootGroup, true, "");
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.result.success = true;
|
||||||
|
return Promise.resolve(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(node: Element, isRootNode: boolean, groupPrefixName: string) {
|
||||||
|
const folderIndex = this.result.folders.length;
|
||||||
|
let groupName = groupPrefixName;
|
||||||
|
|
||||||
|
if (!isRootNode) {
|
||||||
|
if (groupName !== "") {
|
||||||
|
groupName += "/";
|
||||||
|
}
|
||||||
|
const nameEl = this.querySelectorDirectChild(node, "Name");
|
||||||
|
groupName += nameEl == null ? "-" : nameEl.textContent;
|
||||||
|
const folder = new FolderView();
|
||||||
|
folder.name = groupName;
|
||||||
|
this.result.folders.push(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.querySelectorAllDirectChild(node, "Entry").forEach((entry) => {
|
||||||
|
const cipherIndex = this.result.ciphers.length;
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
this.querySelectorAllDirectChild(entry, "String").forEach((entryString) => {
|
||||||
|
const valueEl = this.querySelectorDirectChild(entryString, "Value");
|
||||||
|
const value = valueEl != null ? valueEl.textContent : null;
|
||||||
|
if (this.isNullOrWhitespace(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keyEl = this.querySelectorDirectChild(entryString, "Key");
|
||||||
|
const key = keyEl != null ? keyEl.textContent : null;
|
||||||
|
|
||||||
|
if (key === "URL") {
|
||||||
|
cipher.login.uris = this.makeUriArray(value);
|
||||||
|
} else if (key === "UserName") {
|
||||||
|
cipher.login.username = value;
|
||||||
|
} else if (key === "Password") {
|
||||||
|
cipher.login.password = value;
|
||||||
|
} else if (key === "otp") {
|
||||||
|
cipher.login.totp = value.replace("key=", "");
|
||||||
|
} else if (key === "Title") {
|
||||||
|
cipher.name = value;
|
||||||
|
} else if (key === "Notes") {
|
||||||
|
cipher.notes += value + "\n";
|
||||||
|
} else {
|
||||||
|
let type = FieldType.Text;
|
||||||
|
const attrs = valueEl.attributes as any;
|
||||||
|
if (
|
||||||
|
attrs.length > 0 &&
|
||||||
|
attrs.ProtectInMemory != null &&
|
||||||
|
attrs.ProtectInMemory.value === "True"
|
||||||
|
) {
|
||||||
|
type = FieldType.Hidden;
|
||||||
|
}
|
||||||
|
this.processKvp(cipher, key, value, type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
this.result.ciphers.push(cipher);
|
||||||
|
|
||||||
|
if (!isRootNode) {
|
||||||
|
this.result.folderRelationships.push([cipherIndex, folderIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.querySelectorAllDirectChild(node, "Group").forEach((group) => {
|
||||||
|
this.traverse(group, false, groupName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
44
jslib/common/src/importers/keepassxCsvImporter.ts
Normal file
44
jslib/common/src/importers/keepassxCsvImporter.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class KeePassXCsvImporter 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.isNullOrWhitespace(value.Title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Group =
|
||||||
|
!this.isNullOrWhitespace(value.Group) && value.Group.startsWith("Root/")
|
||||||
|
? value.Group.replace("Root/", "")
|
||||||
|
: value.Group;
|
||||||
|
const groupName = !this.isNullOrWhitespace(value.Group) ? value.Group : null;
|
||||||
|
this.processFolder(result, groupName);
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.notes = this.getValueOrDefault(value.Notes);
|
||||||
|
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);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ImportResult } from "../../models/domain/importResult";
|
||||||
|
import { BaseImporter } from "../baseImporter";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
export class KeeperCsvImporter 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 < 6) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processFolder(result, value[0]);
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.notes = this.getValueOrDefault(value[5]) + "\n";
|
||||||
|
cipher.name = this.getValueOrDefault(value[1], "--");
|
||||||
|
cipher.login.username = this.getValueOrDefault(value[2]);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value[3]);
|
||||||
|
cipher.login.uris = this.makeUriArray(value[4]);
|
||||||
|
|
||||||
|
if (value.length > 7) {
|
||||||
|
// we have some custom fields.
|
||||||
|
for (let i = 7; i < value.length; i = i + 2) {
|
||||||
|
this.processKvp(cipher, value[i], value[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { ImportResult } from "../../models/domain/importResult";
|
||||||
|
import { BaseImporter } from "../baseImporter";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
import { KeeperJsonExport, RecordsEntity } from "./types/keeperJsonTypes";
|
||||||
|
|
||||||
|
export class KeeperJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const keeperExport: KeeperJsonExport = JSON.parse(data);
|
||||||
|
if (keeperExport == null || keeperExport.records == null || keeperExport.records.length === 0) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
keeperExport.records.forEach((record) => {
|
||||||
|
this.parseFolders(result, record);
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = record.title;
|
||||||
|
cipher.login.username = record.login;
|
||||||
|
cipher.login.password = record.password;
|
||||||
|
|
||||||
|
cipher.login.uris = this.makeUriArray(record.login_url);
|
||||||
|
cipher.notes = record.notes;
|
||||||
|
|
||||||
|
if (record.custom_fields != null) {
|
||||||
|
let customfieldKeys = Object.keys(record.custom_fields);
|
||||||
|
if (record.custom_fields["TFC:Keeper"] != null) {
|
||||||
|
customfieldKeys = customfieldKeys.filter((item) => item !== "TFC:Keeper");
|
||||||
|
cipher.login.totp = record.custom_fields["TFC:Keeper"];
|
||||||
|
}
|
||||||
|
|
||||||
|
customfieldKeys.forEach((key) => {
|
||||||
|
this.processKvp(cipher, key, record.custom_fields[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFolders(result: ImportResult, record: RecordsEntity) {
|
||||||
|
if (record.folders == null || record.folders.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
record.folders.forEach((item) => {
|
||||||
|
if (item.folder != null) {
|
||||||
|
this.processFolder(result, item.folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.shared_folder != null) {
|
||||||
|
this.processFolder(result, item.shared_folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export interface KeeperJsonExport {
|
||||||
|
shared_folders?: SharedFoldersEntity[] | null;
|
||||||
|
records?: RecordsEntity[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharedFoldersEntity {
|
||||||
|
path: string;
|
||||||
|
manage_users: boolean;
|
||||||
|
manage_records: boolean;
|
||||||
|
can_edit: boolean;
|
||||||
|
can_share: boolean;
|
||||||
|
permissions?: PermissionsEntity[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionsEntity {
|
||||||
|
uid?: string | null;
|
||||||
|
manage_users: boolean;
|
||||||
|
manage_records: boolean;
|
||||||
|
name?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordsEntity {
|
||||||
|
title: string;
|
||||||
|
login: string;
|
||||||
|
password: string;
|
||||||
|
login_url: string;
|
||||||
|
notes?: string;
|
||||||
|
custom_fields?: CustomFields;
|
||||||
|
folders?: FoldersEntity[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomFields = {
|
||||||
|
[key: string]: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FoldersEntity {
|
||||||
|
folder?: string | null;
|
||||||
|
shared_folder?: string | null;
|
||||||
|
can_edit?: boolean | null;
|
||||||
|
can_share?: boolean | null;
|
||||||
|
}
|
||||||
285
jslib/common/src/importers/lastpassCsvImporter.ts
Normal file
285
jslib/common/src/importers/lastpassCsvImporter.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
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 { FolderView } from "../models/view/folderView";
|
||||||
|
import { IdentityView } from "../models/view/identityView";
|
||||||
|
import { LoginView } from "../models/view/loginView";
|
||||||
|
import { SecureNoteView } from "../models/view/secureNoteView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class LastPassCsvImporter 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 cipherIndex = result.ciphers.length;
|
||||||
|
let folderIndex = result.folders.length;
|
||||||
|
let grouping = value.grouping;
|
||||||
|
if (grouping != null) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
grouping = grouping.replace(/\\/g, "/").replace(/[\x00-\x1F\x7F-\x9F]/g, "");
|
||||||
|
}
|
||||||
|
const hasFolder = this.getValueOrDefault(grouping, "(none)") !== "(none)";
|
||||||
|
let addFolder = hasFolder;
|
||||||
|
|
||||||
|
if (hasFolder) {
|
||||||
|
for (let i = 0; i < result.folders.length; i++) {
|
||||||
|
if (result.folders[i].name === grouping) {
|
||||||
|
addFolder = false;
|
||||||
|
folderIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.buildBaseCipher(value);
|
||||||
|
if (cipher.type === CipherType.Login) {
|
||||||
|
cipher.notes = this.getValueOrDefault(value.extra);
|
||||||
|
cipher.login = new LoginView();
|
||||||
|
cipher.login.uris = this.makeUriArray(value.url);
|
||||||
|
cipher.login.username = this.getValueOrDefault(value.username);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value.password);
|
||||||
|
cipher.login.totp = this.getValueOrDefault(value.totp);
|
||||||
|
} else if (cipher.type === CipherType.SecureNote) {
|
||||||
|
this.parseSecureNote(value, cipher);
|
||||||
|
} else if (cipher.type === CipherType.Card) {
|
||||||
|
cipher.card = this.parseCard(value);
|
||||||
|
cipher.notes = this.getValueOrDefault(value.notes);
|
||||||
|
} else if (cipher.type === CipherType.Identity) {
|
||||||
|
cipher.identity = this.parseIdentity(value);
|
||||||
|
cipher.notes = this.getValueOrDefault(value.notes);
|
||||||
|
if (!this.isNullOrWhitespace(value.ccnum)) {
|
||||||
|
// there is a card on this identity too
|
||||||
|
const cardCipher = this.buildBaseCipher(value);
|
||||||
|
cardCipher.identity = null;
|
||||||
|
cardCipher.type = CipherType.Card;
|
||||||
|
cardCipher.card = this.parseCard(value);
|
||||||
|
result.ciphers.push(cardCipher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
|
||||||
|
if (addFolder) {
|
||||||
|
const f = new FolderView();
|
||||||
|
f.name = grouping;
|
||||||
|
result.folders.push(f);
|
||||||
|
}
|
||||||
|
if (hasFolder) {
|
||||||
|
result.folderRelationships.push([cipherIndex, folderIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBaseCipher(value: any) {
|
||||||
|
const cipher = new CipherView();
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (value.hasOwnProperty("profilename") && value.hasOwnProperty("profilelanguage")) {
|
||||||
|
// form fill
|
||||||
|
cipher.favorite = false;
|
||||||
|
cipher.name = this.getValueOrDefault(value.profilename, "--");
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.isNullOrWhitespace(value.title) ||
|
||||||
|
!this.isNullOrWhitespace(value.firstname) ||
|
||||||
|
!this.isNullOrWhitespace(value.lastname) ||
|
||||||
|
!this.isNullOrWhitespace(value.address1) ||
|
||||||
|
!this.isNullOrWhitespace(value.phone) ||
|
||||||
|
!this.isNullOrWhitespace(value.username) ||
|
||||||
|
!this.isNullOrWhitespace(value.email)
|
||||||
|
) {
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// site or secure note
|
||||||
|
cipher.favorite = !this.organization && this.getValueOrDefault(value.fav, "0") === "1";
|
||||||
|
cipher.name = this.getValueOrDefault(value.name, "--");
|
||||||
|
cipher.type = value.url === "http://sn" ? CipherType.SecureNote : CipherType.Login;
|
||||||
|
}
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCard(value: any): CardView {
|
||||||
|
const card = new CardView();
|
||||||
|
card.cardholderName = this.getValueOrDefault(value.ccname);
|
||||||
|
card.number = this.getValueOrDefault(value.ccnum);
|
||||||
|
card.code = this.getValueOrDefault(value.cccsc);
|
||||||
|
card.brand = this.getCardBrand(value.ccnum);
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(value.ccexp) && value.ccexp.indexOf("-") > -1) {
|
||||||
|
const ccexpParts = (value.ccexp as string).split("-");
|
||||||
|
if (ccexpParts.length > 1) {
|
||||||
|
card.expYear = ccexpParts[0];
|
||||||
|
card.expMonth = ccexpParts[1];
|
||||||
|
if (card.expMonth.length === 2 && card.expMonth[0] === "0") {
|
||||||
|
card.expMonth = card.expMonth[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseIdentity(value: any): IdentityView {
|
||||||
|
const identity = new IdentityView();
|
||||||
|
identity.title = this.getValueOrDefault(value.title);
|
||||||
|
identity.firstName = this.getValueOrDefault(value.firstname);
|
||||||
|
identity.middleName = this.getValueOrDefault(value.middlename);
|
||||||
|
identity.lastName = this.getValueOrDefault(value.lastname);
|
||||||
|
identity.username = this.getValueOrDefault(value.username);
|
||||||
|
identity.company = this.getValueOrDefault(value.company);
|
||||||
|
identity.ssn = this.getValueOrDefault(value.ssn);
|
||||||
|
identity.address1 = this.getValueOrDefault(value.address1);
|
||||||
|
identity.address2 = this.getValueOrDefault(value.address2);
|
||||||
|
identity.address3 = this.getValueOrDefault(value.address3);
|
||||||
|
identity.city = this.getValueOrDefault(value.city);
|
||||||
|
identity.state = this.getValueOrDefault(value.state);
|
||||||
|
identity.postalCode = this.getValueOrDefault(value.zip);
|
||||||
|
identity.country = this.getValueOrDefault(value.country);
|
||||||
|
identity.email = this.getValueOrDefault(value.email);
|
||||||
|
identity.phone = this.getValueOrDefault(value.phone);
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(identity.title)) {
|
||||||
|
identity.title = identity.title.charAt(0).toUpperCase() + identity.title.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSecureNote(value: any, cipher: CipherView) {
|
||||||
|
const extraParts = this.splitNewLine(value.extra);
|
||||||
|
let processedNote = false;
|
||||||
|
|
||||||
|
if (extraParts.length) {
|
||||||
|
const typeParts = extraParts[0].split(":");
|
||||||
|
if (
|
||||||
|
typeParts.length > 1 &&
|
||||||
|
typeParts[0] === "NoteType" &&
|
||||||
|
(typeParts[1] === "Credit Card" || typeParts[1] === "Address")
|
||||||
|
) {
|
||||||
|
if (typeParts[1] === "Credit Card") {
|
||||||
|
const mappedData = this.parseSecureNoteMapping<CardView>(cipher, extraParts, {
|
||||||
|
Number: "number",
|
||||||
|
"Name on Card": "cardholderName",
|
||||||
|
"Security Code": "code",
|
||||||
|
// LP provides date in a format like 'June,2020'
|
||||||
|
// Store in expMonth, then parse and modify
|
||||||
|
"Expiration Date": "expMonth",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(mappedData.expMonth) || mappedData.expMonth === ",") {
|
||||||
|
// No expiration data
|
||||||
|
mappedData.expMonth = undefined;
|
||||||
|
} else {
|
||||||
|
const [monthString, year] = mappedData.expMonth.split(",");
|
||||||
|
// Parse month name into number
|
||||||
|
if (!this.isNullOrWhitespace(monthString)) {
|
||||||
|
const month = new Date(Date.parse(monthString.trim() + " 1, 2012")).getMonth() + 1;
|
||||||
|
if (isNaN(month)) {
|
||||||
|
mappedData.expMonth = undefined;
|
||||||
|
} else {
|
||||||
|
mappedData.expMonth = month.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mappedData.expMonth = undefined;
|
||||||
|
}
|
||||||
|
if (!this.isNullOrWhitespace(year)) {
|
||||||
|
mappedData.expYear = year;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = mappedData;
|
||||||
|
} else if (typeParts[1] === "Address") {
|
||||||
|
const mappedData = this.parseSecureNoteMapping<IdentityView>(cipher, extraParts, {
|
||||||
|
Title: "title",
|
||||||
|
"First Name": "firstName",
|
||||||
|
"Last Name": "lastName",
|
||||||
|
"Middle Name": "middleName",
|
||||||
|
Company: "company",
|
||||||
|
"Address 1": "address1",
|
||||||
|
"Address 2": "address2",
|
||||||
|
"Address 3": "address3",
|
||||||
|
"City / Town": "city",
|
||||||
|
State: "state",
|
||||||
|
"Zip / Postal Code": "postalCode",
|
||||||
|
Country: "country",
|
||||||
|
"Email Address": "email",
|
||||||
|
Username: "username",
|
||||||
|
});
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity = mappedData;
|
||||||
|
}
|
||||||
|
processedNote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!processedNote) {
|
||||||
|
cipher.secureNote = new SecureNoteView();
|
||||||
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
cipher.notes = this.getValueOrDefault(value.extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSecureNoteMapping<T>(cipher: CipherView, extraParts: string[], map: any): T {
|
||||||
|
const dataObj: any = {};
|
||||||
|
|
||||||
|
let processingNotes = false;
|
||||||
|
extraParts.forEach((extraPart) => {
|
||||||
|
let key: string = null;
|
||||||
|
let val: string = null;
|
||||||
|
if (!processingNotes) {
|
||||||
|
if (this.isNullOrWhitespace(extraPart)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const colonIndex = extraPart.indexOf(":");
|
||||||
|
if (colonIndex === -1) {
|
||||||
|
key = extraPart;
|
||||||
|
} else {
|
||||||
|
key = extraPart.substring(0, colonIndex);
|
||||||
|
if (extraPart.length > colonIndex) {
|
||||||
|
val = extraPart.substring(colonIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.isNullOrWhitespace(key) || this.isNullOrWhitespace(val) || key === "NoteType") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processingNotes) {
|
||||||
|
cipher.notes += "\n" + extraPart;
|
||||||
|
} else if (key === "Notes") {
|
||||||
|
if (!this.isNullOrWhitespace(cipher.notes)) {
|
||||||
|
cipher.notes += "\n" + val;
|
||||||
|
} else {
|
||||||
|
cipher.notes = val;
|
||||||
|
}
|
||||||
|
processingNotes = true;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} else if (map.hasOwnProperty(key)) {
|
||||||
|
dataObj[map[key]] = val;
|
||||||
|
} else {
|
||||||
|
this.processKvp(cipher, key, val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
jslib/common/src/importers/logMeOnceCsvImporter.ts
Normal file
31
jslib/common/src/importers/logMeOnceCsvImporter.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class LogMeOnceCsvImporter 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 < 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(value[0], "--");
|
||||||
|
cipher.login.username = this.getValueOrDefault(value[2]);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value[3]);
|
||||||
|
cipher.login.uris = this.makeUriArray(value[1]);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
jslib/common/src/importers/meldiumCsvImporter.ts
Normal file
29
jslib/common/src/importers/meldiumCsvImporter.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class MeldiumCsvImporter 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.DisplayName, "--");
|
||||||
|
cipher.notes = this.getValueOrDefault(value.Notes);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
jslib/common/src/importers/msecureCsvImporter.ts
Normal file
61
jslib/common/src/importers/msecureCsvImporter.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { CipherType } from "../enums/cipherType";
|
||||||
|
import { SecureNoteType } from "../enums/secureNoteType";
|
||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { SecureNoteView } from "../models/view/secureNoteView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class MSecureCsvImporter 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 < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderName =
|
||||||
|
this.getValueOrDefault(value[0], "Unassigned") !== "Unassigned" ? value[0] : null;
|
||||||
|
this.processFolder(result, folderName);
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(value[2], "--");
|
||||||
|
|
||||||
|
if (value[1] === "Web Logins" || value[1] === "Login") {
|
||||||
|
cipher.login.uris = this.makeUriArray(value[4]);
|
||||||
|
cipher.login.username = this.getValueOrDefault(value[5]);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value[6]);
|
||||||
|
cipher.notes = !this.isNullOrWhitespace(value[3]) ? value[3].split("\\n").join("\n") : null;
|
||||||
|
} else if (value.length > 3) {
|
||||||
|
cipher.type = CipherType.SecureNote;
|
||||||
|
cipher.secureNote = new SecureNoteView();
|
||||||
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
for (let i = 3; i < value.length; i++) {
|
||||||
|
if (!this.isNullOrWhitespace(value[i])) {
|
||||||
|
cipher.notes += value[i] + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(value[1]) && cipher.type !== CipherType.Login) {
|
||||||
|
cipher.name = value[1] + ": " + cipher.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
157
jslib/common/src/importers/mykiCsvImporter.ts
Normal file
157
jslib/common/src/importers/mykiCsvImporter.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
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 mappedBaseColumns = ["nickname", "additionalInfo"];
|
||||||
|
const _mappedUserAccountColumns = new Set(
|
||||||
|
mappedBaseColumns.concat(["url", "username", "password", "twofaSecret"])
|
||||||
|
);
|
||||||
|
const _mappedCreditCardColumns = new Set(
|
||||||
|
mappedBaseColumns.concat(["cardNumber", "cardName", "exp_month", "exp_year", "cvv"])
|
||||||
|
);
|
||||||
|
|
||||||
|
const _mappedIdentityColumns = new Set(
|
||||||
|
mappedBaseColumns.concat([
|
||||||
|
"title",
|
||||||
|
"firstName",
|
||||||
|
"middleName",
|
||||||
|
"lastName",
|
||||||
|
"email",
|
||||||
|
"firstAddressLine",
|
||||||
|
"secondAddressLine",
|
||||||
|
"city",
|
||||||
|
"country",
|
||||||
|
"zipCode",
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const _mappedIdCardColumns = new Set(mappedBaseColumns.concat(["idName", "idNumber", "idCountry"]));
|
||||||
|
|
||||||
|
const _mappedTwoFaColumns = new Set(mappedBaseColumns.concat(["authToken"]));
|
||||||
|
|
||||||
|
const _mappedUserNoteColumns = new Set(mappedBaseColumns.concat(["content"]));
|
||||||
|
|
||||||
|
export class MykiCsvImporter 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.nickname, "--");
|
||||||
|
cipher.notes = this.getValueOrDefault(value.additionalInfo);
|
||||||
|
|
||||||
|
if (value.url !== undefined) {
|
||||||
|
// Accounts
|
||||||
|
cipher.login.uris = this.makeUriArray(value.url);
|
||||||
|
cipher.login.username = this.getValueOrDefault(value.username);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value.password);
|
||||||
|
cipher.login.totp = this.getValueOrDefault(value.twofaSecret);
|
||||||
|
|
||||||
|
this.importUnmappedFields(cipher, value, _mappedUserAccountColumns);
|
||||||
|
} else if (value.authToken !== undefined) {
|
||||||
|
// TwoFA
|
||||||
|
cipher.login.totp = this.getValueOrDefault(value.authToken);
|
||||||
|
|
||||||
|
this.importUnmappedFields(cipher, value, _mappedTwoFaColumns);
|
||||||
|
} else if (value.cardNumber !== undefined) {
|
||||||
|
// Cards
|
||||||
|
cipher.card = new CardView();
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card.cardholderName = this.getValueOrDefault(value.cardName);
|
||||||
|
cipher.card.number = this.getValueOrDefault(value.cardNumber);
|
||||||
|
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||||
|
cipher.card.expMonth = this.getValueOrDefault(value.exp_month);
|
||||||
|
cipher.card.expYear = this.getValueOrDefault(value.exp_year);
|
||||||
|
cipher.card.code = this.getValueOrDefault(value.cvv);
|
||||||
|
|
||||||
|
this.importUnmappedFields(cipher, value, _mappedCreditCardColumns);
|
||||||
|
} else if (value.firstName !== undefined) {
|
||||||
|
// Identities
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity.title = this.getValueOrDefault(value.title);
|
||||||
|
cipher.identity.firstName = this.getValueOrDefault(value.firstName);
|
||||||
|
cipher.identity.middleName = this.getValueOrDefault(value.middleName);
|
||||||
|
cipher.identity.lastName = this.getValueOrDefault(value.lastName);
|
||||||
|
cipher.identity.phone = this.getValueOrDefault(value.number);
|
||||||
|
cipher.identity.email = this.getValueOrDefault(value.email);
|
||||||
|
cipher.identity.address1 = this.getValueOrDefault(value.firstAddressLine);
|
||||||
|
cipher.identity.address2 = this.getValueOrDefault(value.secondAddressLine);
|
||||||
|
cipher.identity.city = this.getValueOrDefault(value.city);
|
||||||
|
cipher.identity.country = this.getValueOrDefault(value.country);
|
||||||
|
cipher.identity.postalCode = this.getValueOrDefault(value.zipCode);
|
||||||
|
|
||||||
|
this.importUnmappedFields(cipher, value, _mappedIdentityColumns);
|
||||||
|
} else if (value.idType !== undefined) {
|
||||||
|
// IdCards
|
||||||
|
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
this.processFullName(cipher, value.idName);
|
||||||
|
cipher.identity.country = this.getValueOrDefault(value.idCountry);
|
||||||
|
|
||||||
|
switch (value.idType) {
|
||||||
|
// case "Driver's License":
|
||||||
|
// case "ID Card":
|
||||||
|
// case "Outdoor License":
|
||||||
|
// case "Software License":
|
||||||
|
// case "Tax Number":
|
||||||
|
// case "Bank Account":
|
||||||
|
// case "Insurance Card":
|
||||||
|
// case "Health Card":
|
||||||
|
// case "Membership":
|
||||||
|
// case "Database":
|
||||||
|
// case "Reward Program":
|
||||||
|
// case "Tour Visa":
|
||||||
|
case "Passport":
|
||||||
|
cipher.identity.passportNumber = value.idNumber;
|
||||||
|
break;
|
||||||
|
case "Social Security":
|
||||||
|
cipher.identity.ssn = value.idNumber;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cipher.identity.licenseNumber = value.idNumber;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importUnmappedFields(cipher, value, _mappedIdCardColumns);
|
||||||
|
} else if (value.content !== undefined) {
|
||||||
|
// Notes
|
||||||
|
cipher.secureNote = new SecureNoteView();
|
||||||
|
cipher.type = CipherType.SecureNote;
|
||||||
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
cipher.notes = this.getValueOrDefault(value.content);
|
||||||
|
|
||||||
|
this.importUnmappedFields(cipher, value, _mappedUserNoteColumns);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
127
jslib/common/src/importers/nordpassCsvImporter.ts
Normal file
127
jslib/common/src/importers/nordpassCsvImporter.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { CipherType } from "../enums/cipherType";
|
||||||
|
import { SecureNoteType } from "../enums/secureNoteType";
|
||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { CipherView } from "../models/view/cipherView";
|
||||||
|
import { LoginView } from "../models/view/loginView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
type nodePassCsvParsed = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
note: string;
|
||||||
|
cardholdername: string;
|
||||||
|
cardnumber: string;
|
||||||
|
cvc: string;
|
||||||
|
expirydate: string;
|
||||||
|
zipcode: string;
|
||||||
|
folder: string;
|
||||||
|
full_name: string;
|
||||||
|
phone_number: string;
|
||||||
|
email: string;
|
||||||
|
address1: string;
|
||||||
|
address2: string;
|
||||||
|
city: string;
|
||||||
|
country: string;
|
||||||
|
state: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NordPassCsvImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results: nodePassCsvParsed[] = this.parseCsv(data, true);
|
||||||
|
if (results == null) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach((record) => {
|
||||||
|
const recordType = this.evaluateType(record);
|
||||||
|
if (recordType === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.organization) {
|
||||||
|
this.processFolder(result, record.folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = new CipherView();
|
||||||
|
cipher.name = this.getValueOrDefault(record.name, "--");
|
||||||
|
cipher.notes = this.getValueOrDefault(record.note);
|
||||||
|
|
||||||
|
switch (recordType) {
|
||||||
|
case CipherType.Login:
|
||||||
|
cipher.type = CipherType.Login;
|
||||||
|
cipher.login = new LoginView();
|
||||||
|
cipher.login.username = this.getValueOrDefault(record.username);
|
||||||
|
cipher.login.password = this.getValueOrDefault(record.password);
|
||||||
|
cipher.login.uris = this.makeUriArray(record.url);
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card.cardholderName = this.getValueOrDefault(record.cardholdername);
|
||||||
|
cipher.card.number = this.getValueOrDefault(record.cardnumber);
|
||||||
|
cipher.card.code = this.getValueOrDefault(record.cvc);
|
||||||
|
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||||
|
this.setCardExpiration(cipher, record.expirydate);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CipherType.Identity:
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
|
||||||
|
this.processFullName(cipher, this.getValueOrDefault(record.full_name));
|
||||||
|
cipher.identity.address1 = this.getValueOrDefault(record.address1);
|
||||||
|
cipher.identity.address2 = this.getValueOrDefault(record.address2);
|
||||||
|
cipher.identity.city = this.getValueOrDefault(record.city);
|
||||||
|
cipher.identity.state = this.getValueOrDefault(record.state);
|
||||||
|
cipher.identity.postalCode = this.getValueOrDefault(record.zipcode);
|
||||||
|
cipher.identity.country = this.getValueOrDefault(record.country);
|
||||||
|
if (cipher.identity.country != null) {
|
||||||
|
cipher.identity.country = cipher.identity.country.toUpperCase();
|
||||||
|
}
|
||||||
|
cipher.identity.email = this.getValueOrDefault(record.email);
|
||||||
|
cipher.identity.phone = this.getValueOrDefault(record.phone_number);
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
cipher.type = CipherType.SecureNote;
|
||||||
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private evaluateType(record: nodePassCsvParsed): CipherType {
|
||||||
|
if (!this.isNullOrWhitespace(record.username)) {
|
||||||
|
return CipherType.Login;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(record.cardnumber)) {
|
||||||
|
return CipherType.Card;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(record.full_name)) {
|
||||||
|
return CipherType.Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(record.note)) {
|
||||||
|
return CipherType.SecureNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { CipherView } from "../../models/view/cipherView";
|
||||||
|
|
||||||
|
export class CipherImportContext {
|
||||||
|
lowerProperty: string;
|
||||||
|
constructor(public importRecord: any, public property: string, public cipher: CipherView) {
|
||||||
|
this.lowerProperty = property.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { CipherType } from "../../enums/cipherType";
|
||||||
|
import { FieldType } from "../../enums/fieldType";
|
||||||
|
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 { PasswordHistoryView } from "../../models/view/passwordHistoryView";
|
||||||
|
import { SecureNoteView } from "../../models/view/secureNoteView";
|
||||||
|
import { BaseImporter } from "../baseImporter";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
export class OnePassword1PifImporter extends BaseImporter implements Importer {
|
||||||
|
result = new ImportResult();
|
||||||
|
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
data.split(this.newLineRegex).forEach((line) => {
|
||||||
|
if (this.isNullOrWhitespace(line) || line[0] !== "{") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = JSON.parse(line);
|
||||||
|
if (item.trashed === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(item.hmac)) {
|
||||||
|
this.processStandardItem(item, cipher);
|
||||||
|
} else {
|
||||||
|
this.processWinOpVaultItem(item, cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
this.result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.result.success = true;
|
||||||
|
return Promise.resolve(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processWinOpVaultItem(item: any, cipher: CipherView) {
|
||||||
|
if (item.overview != null) {
|
||||||
|
cipher.name = this.getValueOrDefault(item.overview.title);
|
||||||
|
if (item.overview.URLs != null) {
|
||||||
|
const urls: string[] = [];
|
||||||
|
item.overview.URLs.forEach((url: any) => {
|
||||||
|
if (!this.isNullOrWhitespace(url.u)) {
|
||||||
|
urls.push(url.u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cipher.login.uris = this.makeUriArray(urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.details != null) {
|
||||||
|
if (item.details.passwordHistory != null) {
|
||||||
|
this.parsePasswordHistory(item.details.passwordHistory, cipher);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.isNullOrWhitespace(item.details.ccnum) ||
|
||||||
|
!this.isNullOrWhitespace(item.details.cvv)
|
||||||
|
) {
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = new CardView();
|
||||||
|
} else if (
|
||||||
|
!this.isNullOrWhitespace(item.details.firstname) ||
|
||||||
|
!this.isNullOrWhitespace(item.details.address1)
|
||||||
|
) {
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
}
|
||||||
|
if (cipher.type === CipherType.Login && !this.isNullOrWhitespace(item.details.password)) {
|
||||||
|
cipher.login.password = item.details.password;
|
||||||
|
}
|
||||||
|
if (!this.isNullOrWhitespace(item.details.notesPlain)) {
|
||||||
|
cipher.notes = item.details.notesPlain.split(this.newLineRegex).join("\n") + "\n";
|
||||||
|
}
|
||||||
|
if (item.details.fields != null) {
|
||||||
|
this.parseFields(item.details.fields, cipher, "designation", "value", "name");
|
||||||
|
}
|
||||||
|
if (item.details.sections != null) {
|
||||||
|
item.details.sections.forEach((section: any) => {
|
||||||
|
if (section.fields != null) {
|
||||||
|
this.parseFields(section.fields, cipher, "n", "v", "t");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processStandardItem(item: any, cipher: CipherView) {
|
||||||
|
cipher.favorite = item.openContents && item.openContents.faveIndex ? true : false;
|
||||||
|
cipher.name = this.getValueOrDefault(item.title);
|
||||||
|
|
||||||
|
if (item.typeName === "securenotes.SecureNote") {
|
||||||
|
cipher.type = CipherType.SecureNote;
|
||||||
|
cipher.secureNote = new SecureNoteView();
|
||||||
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
} else if (item.typeName === "wallet.financial.CreditCard") {
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = new CardView();
|
||||||
|
} else if (item.typeName === "identities.Identity") {
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
} else {
|
||||||
|
cipher.login.uris = this.makeUriArray(item.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.secureContents != null) {
|
||||||
|
if (item.secureContents.passwordHistory != null) {
|
||||||
|
this.parsePasswordHistory(item.secureContents.passwordHistory, cipher);
|
||||||
|
}
|
||||||
|
if (!this.isNullOrWhitespace(item.secureContents.notesPlain)) {
|
||||||
|
cipher.notes = item.secureContents.notesPlain.split(this.newLineRegex).join("\n") + "\n";
|
||||||
|
}
|
||||||
|
if (cipher.type === CipherType.Login) {
|
||||||
|
if (!this.isNullOrWhitespace(item.secureContents.password)) {
|
||||||
|
cipher.login.password = item.secureContents.password;
|
||||||
|
}
|
||||||
|
if (item.secureContents.URLs != null) {
|
||||||
|
const urls: string[] = [];
|
||||||
|
item.secureContents.URLs.forEach((u: any) => {
|
||||||
|
if (!this.isNullOrWhitespace(u.url)) {
|
||||||
|
urls.push(u.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (urls.length > 0) {
|
||||||
|
cipher.login.uris = this.makeUriArray(urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.secureContents.fields != null) {
|
||||||
|
this.parseFields(item.secureContents.fields, cipher, "designation", "value", "name");
|
||||||
|
}
|
||||||
|
if (item.secureContents.sections != null) {
|
||||||
|
item.secureContents.sections.forEach((section: any) => {
|
||||||
|
if (section.fields != null) {
|
||||||
|
this.parseFields(section.fields, cipher, "n", "v", "t");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePasswordHistory(items: any[], cipher: CipherView) {
|
||||||
|
const maxSize = items.length > 5 ? 5 : items.length;
|
||||||
|
cipher.passwordHistory = items
|
||||||
|
.filter((h: any) => !this.isNullOrWhitespace(h.value) && h.time != null)
|
||||||
|
.sort((a, b) => b.time - a.time)
|
||||||
|
.slice(0, maxSize)
|
||||||
|
.map((h: any) => {
|
||||||
|
const ph = new PasswordHistoryView();
|
||||||
|
ph.password = h.value;
|
||||||
|
ph.lastUsedDate = new Date(("" + h.time).length >= 13 ? h.time : h.time * 1000);
|
||||||
|
return ph;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFields(
|
||||||
|
fields: any[],
|
||||||
|
cipher: CipherView,
|
||||||
|
designationKey: string,
|
||||||
|
valueKey: string,
|
||||||
|
nameKey: string
|
||||||
|
) {
|
||||||
|
fields.forEach((field: any) => {
|
||||||
|
if (field[valueKey] == null || field[valueKey].toString().trim() === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when date FieldType exists, store this as a date field type instead of formatted Text if k is 'date'
|
||||||
|
const fieldValue =
|
||||||
|
field.k === "date"
|
||||||
|
? new Date(field[valueKey] * 1000).toUTCString()
|
||||||
|
: field[valueKey].toString();
|
||||||
|
const fieldDesignation =
|
||||||
|
field[designationKey] != null ? field[designationKey].toString() : null;
|
||||||
|
|
||||||
|
if (cipher.type === CipherType.Login) {
|
||||||
|
if (this.isNullOrWhitespace(cipher.login.username) && fieldDesignation === "username") {
|
||||||
|
cipher.login.username = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
this.isNullOrWhitespace(cipher.login.password) &&
|
||||||
|
fieldDesignation === "password"
|
||||||
|
) {
|
||||||
|
cipher.login.password = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
this.isNullOrWhitespace(cipher.login.totp) &&
|
||||||
|
fieldDesignation != null &&
|
||||||
|
fieldDesignation.startsWith("TOTP_")
|
||||||
|
) {
|
||||||
|
cipher.login.totp = fieldValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (cipher.type === CipherType.Card) {
|
||||||
|
if (this.isNullOrWhitespace(cipher.card.number) && fieldDesignation === "ccnum") {
|
||||||
|
cipher.card.number = fieldValue;
|
||||||
|
cipher.card.brand = this.getCardBrand(fieldValue);
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(cipher.card.code) && fieldDesignation === "cvv") {
|
||||||
|
cipher.card.code = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
this.isNullOrWhitespace(cipher.card.cardholderName) &&
|
||||||
|
fieldDesignation === "cardholder"
|
||||||
|
) {
|
||||||
|
cipher.card.cardholderName = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
this.isNullOrWhitespace(cipher.card.expiration) &&
|
||||||
|
fieldDesignation === "expiry" &&
|
||||||
|
fieldValue.length === 6
|
||||||
|
) {
|
||||||
|
cipher.card.expMonth = (fieldValue as string).substr(4, 2);
|
||||||
|
if (cipher.card.expMonth[0] === "0") {
|
||||||
|
cipher.card.expMonth = cipher.card.expMonth.substr(1, 1);
|
||||||
|
}
|
||||||
|
cipher.card.expYear = (fieldValue as string).substr(0, 4);
|
||||||
|
return;
|
||||||
|
} else if (fieldDesignation === "type") {
|
||||||
|
// Skip since brand was determined from number above
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (cipher.type === CipherType.Identity) {
|
||||||
|
const identity = cipher.identity;
|
||||||
|
if (this.isNullOrWhitespace(identity.firstName) && fieldDesignation === "firstname") {
|
||||||
|
identity.firstName = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(identity.lastName) && fieldDesignation === "lastname") {
|
||||||
|
identity.lastName = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(identity.middleName) && fieldDesignation === "initial") {
|
||||||
|
identity.middleName = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(identity.phone) && fieldDesignation === "defphone") {
|
||||||
|
identity.phone = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(identity.company) && fieldDesignation === "company") {
|
||||||
|
identity.company = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(identity.email) && fieldDesignation === "email") {
|
||||||
|
identity.email = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (this.isNullOrWhitespace(identity.username) && fieldDesignation === "username") {
|
||||||
|
identity.username = fieldValue;
|
||||||
|
return;
|
||||||
|
} else if (fieldDesignation === "address") {
|
||||||
|
// fieldValue is an object casted into a string, so access the plain value instead
|
||||||
|
const { street, city, country, zip } = field[valueKey];
|
||||||
|
identity.address1 = this.getValueOrDefault(street);
|
||||||
|
identity.city = this.getValueOrDefault(city);
|
||||||
|
if (!this.isNullOrWhitespace(country)) {
|
||||||
|
identity.country = country.toUpperCase();
|
||||||
|
}
|
||||||
|
identity.postalCode = this.getValueOrDefault(zip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = this.isNullOrWhitespace(field[nameKey]) ? "no_name" : field[nameKey];
|
||||||
|
if (
|
||||||
|
fieldName === "password" &&
|
||||||
|
cipher.passwordHistory != null &&
|
||||||
|
cipher.passwordHistory.some((h) => h.password === fieldValue)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldType = field.k === "concealed" ? FieldType.Hidden : FieldType.Text;
|
||||||
|
this.processKvp(cipher, fieldName, fieldValue, fieldType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,637 @@
|
|||||||
|
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 { CardView } from "../../models/view/cardView";
|
||||||
|
import { CipherView } from "../../models/view/cipherView";
|
||||||
|
import { IdentityView } from "../../models/view/identityView";
|
||||||
|
import { LoginView } from "../../models/view/loginView";
|
||||||
|
import { PasswordHistoryView } from "../../models/view/passwordHistoryView";
|
||||||
|
import { SecureNoteView } from "../../models/view/secureNoteView";
|
||||||
|
import { BaseImporter } from "../baseImporter";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CategoryEnum,
|
||||||
|
Details,
|
||||||
|
ExportData,
|
||||||
|
FieldsEntity,
|
||||||
|
Item,
|
||||||
|
LoginFieldTypeEnum,
|
||||||
|
Overview,
|
||||||
|
PasswordHistoryEntity,
|
||||||
|
SectionsEntity,
|
||||||
|
UrlsEntity,
|
||||||
|
Value,
|
||||||
|
VaultsEntity,
|
||||||
|
} from "./types/onepassword1PuxImporterTypes";
|
||||||
|
|
||||||
|
export class OnePassword1PuxImporter extends BaseImporter implements Importer {
|
||||||
|
result = new ImportResult();
|
||||||
|
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const exportData: ExportData = JSON.parse(data);
|
||||||
|
|
||||||
|
const account = exportData.accounts[0];
|
||||||
|
// TODO Add handling of multiple vaults
|
||||||
|
// const personalVaults = account.vaults[0].filter((v) => v.attrs.type === VaultAttributeTypeEnum.Personal);
|
||||||
|
account.vaults.forEach((vault: VaultsEntity) => {
|
||||||
|
vault.items.forEach((item: Item) => {
|
||||||
|
if (item.trashed === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
|
||||||
|
const category = item.categoryUuid as CategoryEnum;
|
||||||
|
switch (category) {
|
||||||
|
case CategoryEnum.Login:
|
||||||
|
case CategoryEnum.Database:
|
||||||
|
case CategoryEnum.Password:
|
||||||
|
case CategoryEnum.WirelessRouter:
|
||||||
|
case CategoryEnum.Server:
|
||||||
|
case CategoryEnum.API_Credential:
|
||||||
|
cipher.type = CipherType.Login;
|
||||||
|
cipher.login = new LoginView();
|
||||||
|
break;
|
||||||
|
case CategoryEnum.CreditCard:
|
||||||
|
case CategoryEnum.BankAccount:
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = new CardView();
|
||||||
|
break;
|
||||||
|
case CategoryEnum.SecureNote:
|
||||||
|
case CategoryEnum.SoftwareLicense:
|
||||||
|
case CategoryEnum.EmailAccount:
|
||||||
|
case CategoryEnum.MedicalRecord:
|
||||||
|
// case CategoryEnum.Document:
|
||||||
|
cipher.type = CipherType.SecureNote;
|
||||||
|
cipher.secureNote = new SecureNoteView();
|
||||||
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
|
break;
|
||||||
|
case CategoryEnum.Identity:
|
||||||
|
case CategoryEnum.DriversLicense:
|
||||||
|
case CategoryEnum.OutdoorLicense:
|
||||||
|
case CategoryEnum.Membership:
|
||||||
|
case CategoryEnum.Passport:
|
||||||
|
case CategoryEnum.RewardsProgram:
|
||||||
|
case CategoryEnum.SocialSecurityNumber:
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.favorite = item.favIndex === 1 ? true : false;
|
||||||
|
|
||||||
|
this.processOverview(item.overview, cipher);
|
||||||
|
|
||||||
|
this.processLoginFields(item, cipher);
|
||||||
|
|
||||||
|
this.processDetails(category, item.details, cipher);
|
||||||
|
|
||||||
|
this.parsePasswordHistory(item.details.passwordHistory, cipher);
|
||||||
|
|
||||||
|
this.processSections(category, item.details.sections, cipher);
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(item.details.notesPlain)) {
|
||||||
|
cipher.notes = item.details.notesPlain.split(this.newLineRegex).join("\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
this.result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.result.success = true;
|
||||||
|
return Promise.resolve(this.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processOverview(overview: Overview, cipher: CipherView) {
|
||||||
|
if (overview == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.name = this.getValueOrDefault(overview.title);
|
||||||
|
|
||||||
|
if (overview.urls != null) {
|
||||||
|
const urls: string[] = [];
|
||||||
|
overview.urls.forEach((url: UrlsEntity) => {
|
||||||
|
if (!this.isNullOrWhitespace(url.url)) {
|
||||||
|
urls.push(url.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cipher.login.uris = this.makeUriArray(urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overview.tags != null && overview.tags.length > 0) {
|
||||||
|
const folderName = this.capitalize(overview.tags[0]);
|
||||||
|
this.processFolder(this.result, folderName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private capitalize(inputString: string): string {
|
||||||
|
return inputString.trim().replace(/\w\S*/g, (w) => w.replace(/^\w/, (c) => c.toUpperCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private processLoginFields(item: Item, cipher: CipherView) {
|
||||||
|
if (item.details == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.details.loginFields == null || item.details.loginFields.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.details.loginFields.forEach((loginField) => {
|
||||||
|
if (loginField.designation === "username" && loginField.value !== "") {
|
||||||
|
cipher.type = CipherType.Login;
|
||||||
|
cipher.login.username = loginField.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginField.designation === "password" && loginField.value !== "") {
|
||||||
|
cipher.type = CipherType.Login;
|
||||||
|
cipher.login.password = loginField.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fieldValue = loginField.value;
|
||||||
|
let fieldType: FieldType = FieldType.Text;
|
||||||
|
switch (loginField.fieldType) {
|
||||||
|
case LoginFieldTypeEnum.Password:
|
||||||
|
fieldType = FieldType.Hidden;
|
||||||
|
break;
|
||||||
|
case LoginFieldTypeEnum.CheckBox:
|
||||||
|
fieldValue = loginField.value !== "" ? "true" : "false";
|
||||||
|
fieldType = FieldType.Boolean;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.processKvp(cipher, loginField.name, fieldValue, fieldType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processDetails(category: CategoryEnum, details: Details, cipher: CipherView) {
|
||||||
|
if (category !== CategoryEnum.Password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cipher.login.password = details.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
private processSections(category: CategoryEnum, sections: SectionsEntity[], cipher: CipherView) {
|
||||||
|
if (sections == null || sections.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.forEach((section: SectionsEntity) => {
|
||||||
|
if (section.fields == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parseSectionFields(category, section.fields, cipher);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSectionFields(category: CategoryEnum, fields: FieldsEntity[], cipher: CipherView) {
|
||||||
|
fields.forEach((field: FieldsEntity) => {
|
||||||
|
const valueKey = Object.keys(field.value)[0];
|
||||||
|
const anyField = field as any;
|
||||||
|
|
||||||
|
if (
|
||||||
|
anyField.value == null ||
|
||||||
|
anyField.value[valueKey] == null ||
|
||||||
|
anyField.value[valueKey] === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = this.getFieldName(field.id, field.title);
|
||||||
|
const fieldValue = this.extractValue(field.value, valueKey);
|
||||||
|
|
||||||
|
if (cipher.type === CipherType.Login) {
|
||||||
|
if (this.fillLogin(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case CategoryEnum.Login:
|
||||||
|
case CategoryEnum.Database:
|
||||||
|
case CategoryEnum.EmailAccount:
|
||||||
|
case CategoryEnum.WirelessRouter:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CategoryEnum.Server:
|
||||||
|
if (this.isNullOrWhitespace(cipher.login.uri) && field.id === "url") {
|
||||||
|
cipher.login.uris = this.makeUriArray(fieldValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CategoryEnum.API_Credential:
|
||||||
|
if (this.fillApiCredentials(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (cipher.type === CipherType.Card) {
|
||||||
|
if (this.fillCreditCard(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category === CategoryEnum.BankAccount) {
|
||||||
|
if (this.fillBankAccount(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cipher.type === CipherType.Identity) {
|
||||||
|
if (this.fillIdentity(field, fieldValue, cipher, valueKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (valueKey === "address") {
|
||||||
|
// fieldValue is an object casted into a string, so access the plain value instead
|
||||||
|
const { street, city, country, zip, state } = field.value.address;
|
||||||
|
cipher.identity.address1 = this.getValueOrDefault(street);
|
||||||
|
cipher.identity.city = this.getValueOrDefault(city);
|
||||||
|
if (!this.isNullOrWhitespace(country)) {
|
||||||
|
cipher.identity.country = country.toUpperCase();
|
||||||
|
}
|
||||||
|
cipher.identity.postalCode = this.getValueOrDefault(zip);
|
||||||
|
cipher.identity.state = this.getValueOrDefault(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case CategoryEnum.Identity:
|
||||||
|
break;
|
||||||
|
case CategoryEnum.DriversLicense:
|
||||||
|
if (this.fillDriversLicense(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CategoryEnum.OutdoorLicense:
|
||||||
|
if (this.fillOutdoorLicense(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CategoryEnum.Membership:
|
||||||
|
if (this.fillMembership(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CategoryEnum.Passport:
|
||||||
|
if (this.fillPassport(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CategoryEnum.RewardsProgram:
|
||||||
|
if (this.fillRewardsProgram(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CategoryEnum.SocialSecurityNumber:
|
||||||
|
if (this.fillSSN(field, fieldValue, cipher)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueKey === "email") {
|
||||||
|
// fieldValue is an object casted into a string, so access the plain value instead
|
||||||
|
const { email_address, provider } = field.value.email;
|
||||||
|
this.processKvp(cipher, fieldName, email_address, FieldType.Text);
|
||||||
|
this.processKvp(cipher, "provider", provider, FieldType.Text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not include a password field if it's already in the history
|
||||||
|
if (
|
||||||
|
field.title === "password" &&
|
||||||
|
cipher.passwordHistory != null &&
|
||||||
|
cipher.passwordHistory.some((h) => h.password === fieldValue)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO ?? If one of the fields is marked as guarded, then activate Password-Reprompt for the entire item
|
||||||
|
if (field.guarded && cipher.reprompt === CipherRepromptType.None) {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldType = valueKey === "concealed" ? FieldType.Hidden : FieldType.Text;
|
||||||
|
this.processKvp(cipher, fieldName, fieldValue, fieldType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFieldName(id: string, title: string): string {
|
||||||
|
if (this.isNullOrWhitespace(title)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Naive approach of checking if the fields id is usable
|
||||||
|
if (id.length > 25 && RegExp(/[0-9]{2}[A-Z]{2}/, "i").test(id)) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractValue(value: Value, valueKey: string): string {
|
||||||
|
if (valueKey === "date") {
|
||||||
|
return new Date(value.date * 1000).toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueKey === "monthYear") {
|
||||||
|
return value.monthYear.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (value as any)[valueKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillLogin(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
const fieldName = this.getFieldName(field.id, field.title);
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.login.username) && fieldName === "username") {
|
||||||
|
cipher.login.username = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.login.password) && fieldName === "password") {
|
||||||
|
cipher.login.password = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(cipher.login.totp) &&
|
||||||
|
field.id != null &&
|
||||||
|
field.id.startsWith("TOTP_")
|
||||||
|
) {
|
||||||
|
cipher.login.totp = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillApiCredentials(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
const fieldName = this.getFieldName(field.id, field.title);
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.login.password) && fieldName === "credential") {
|
||||||
|
cipher.login.password = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.login.uri) && fieldName === "hostname") {
|
||||||
|
cipher.login.uris = this.makeUriArray(fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillCreditCard(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.card.number) && field.id === "ccnum") {
|
||||||
|
cipher.card.number = fieldValue;
|
||||||
|
cipher.card.brand = this.getCardBrand(fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.card.code) && field.id === "cvv") {
|
||||||
|
cipher.card.code = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.card.cardholderName) && field.id === "cardholder") {
|
||||||
|
cipher.card.cardholderName = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.card.expiration) && field.id === "expiry") {
|
||||||
|
const monthYear: string = fieldValue.toString().trim();
|
||||||
|
cipher.card.expMonth = monthYear.substring(4, 6);
|
||||||
|
if (cipher.card.expMonth[0] === "0") {
|
||||||
|
cipher.card.expMonth = cipher.card.expMonth.substring(1, 2);
|
||||||
|
}
|
||||||
|
cipher.card.expYear = monthYear.substring(0, 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.id === "type") {
|
||||||
|
// Skip since brand was determined from number above
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillBankAccount(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.card.cardholderName) && field.id === "owner") {
|
||||||
|
cipher.card.cardholderName = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillIdentity(
|
||||||
|
field: FieldsEntity,
|
||||||
|
fieldValue: string,
|
||||||
|
cipher: CipherView,
|
||||||
|
valueKey: string
|
||||||
|
): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "firstname") {
|
||||||
|
cipher.identity.firstName = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.lastName) && field.id === "lastname") {
|
||||||
|
cipher.identity.lastName = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.middleName) && field.id === "initial") {
|
||||||
|
cipher.identity.middleName = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.phone) && field.id === "defphone") {
|
||||||
|
cipher.identity.phone = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.company) && field.id === "company") {
|
||||||
|
cipher.identity.company = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.email)) {
|
||||||
|
if (valueKey === "email") {
|
||||||
|
const { email_address, provider } = field.value.email;
|
||||||
|
cipher.identity.email = this.getValueOrDefault(email_address);
|
||||||
|
this.processKvp(cipher, "provider", provider, FieldType.Text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.id === "email") {
|
||||||
|
cipher.identity.email = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.username) && field.id === "username") {
|
||||||
|
cipher.identity.username = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillDriversLicense(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "fullname") {
|
||||||
|
this.processFullName(cipher, fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.address1) && field.id === "address") {
|
||||||
|
cipher.identity.address1 = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO ISO code
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.country) && field.id === "country") {
|
||||||
|
cipher.identity.country = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.state) && field.id === "state") {
|
||||||
|
cipher.identity.state = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.licenseNumber) && field.id === "number") {
|
||||||
|
cipher.identity.licenseNumber = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillOutdoorLicense(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "name") {
|
||||||
|
this.processFullName(cipher, fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO ISO code
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.country) && field.id === "country") {
|
||||||
|
cipher.identity.country = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.state) && field.id === "state") {
|
||||||
|
cipher.identity.state = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillMembership(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "member_name") {
|
||||||
|
this.processFullName(cipher, fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.company) && field.id === "org_name") {
|
||||||
|
cipher.identity.company = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.phone) && field.id === "phone") {
|
||||||
|
cipher.identity.phone = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillPassport(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "fullname") {
|
||||||
|
this.processFullName(cipher, fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Iso
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.country) && field.id === "issuing_country") {
|
||||||
|
cipher.identity.country = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.passportNumber) && field.id === "number") {
|
||||||
|
cipher.identity.passportNumber = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillRewardsProgram(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "member_name") {
|
||||||
|
this.processFullName(cipher, fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.company) && field.id === "company_name") {
|
||||||
|
cipher.identity.company = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillSSN(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.firstName) && field.id === "name") {
|
||||||
|
this.processFullName(cipher, fieldValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNullOrWhitespace(cipher.identity.ssn) && field.id === "number") {
|
||||||
|
cipher.identity.ssn = fieldValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePasswordHistory(historyItems: PasswordHistoryEntity[], cipher: CipherView) {
|
||||||
|
if (historyItems == null || historyItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = historyItems.length > 5 ? 5 : historyItems.length;
|
||||||
|
cipher.passwordHistory = historyItems
|
||||||
|
.filter((h: any) => !this.isNullOrWhitespace(h.value) && h.time != null)
|
||||||
|
.sort((a, b) => b.time - a.time)
|
||||||
|
.slice(0, maxSize)
|
||||||
|
.map((h: any) => {
|
||||||
|
const ph = new PasswordHistoryView();
|
||||||
|
ph.password = h.value;
|
||||||
|
ph.lastUsedDate = new Date(("" + h.time).length >= 13 ? h.time : h.time * 1000);
|
||||||
|
return ph;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
import { CipherType } from "../../enums/cipherType";
|
||||||
|
import { FieldType } from "../../enums/fieldType";
|
||||||
|
import { ImportResult } from "../../models/domain/importResult";
|
||||||
|
import { CipherView } from "../../models/view/cipherView";
|
||||||
|
import { BaseImporter } from "../baseImporter";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
import { CipherImportContext } from "./cipherImportContext";
|
||||||
|
|
||||||
|
export const IgnoredProperties = [
|
||||||
|
"ainfo",
|
||||||
|
"autosubmit",
|
||||||
|
"notesplain",
|
||||||
|
"ps",
|
||||||
|
"scope",
|
||||||
|
"tags",
|
||||||
|
"title",
|
||||||
|
"uuid",
|
||||||
|
"notes",
|
||||||
|
];
|
||||||
|
|
||||||
|
export abstract class OnePasswordCsvImporter extends BaseImporter implements Importer {
|
||||||
|
protected loginPropertyParsers = [
|
||||||
|
this.setLoginUsername,
|
||||||
|
this.setLoginPassword,
|
||||||
|
this.setLoginUris,
|
||||||
|
];
|
||||||
|
protected creditCardPropertyParsers = [
|
||||||
|
this.setCreditCardNumber,
|
||||||
|
this.setCreditCardVerification,
|
||||||
|
this.setCreditCardCardholderName,
|
||||||
|
this.setCreditCardExpiry,
|
||||||
|
];
|
||||||
|
protected identityPropertyParsers = [
|
||||||
|
this.setIdentityFirstName,
|
||||||
|
this.setIdentityInitial,
|
||||||
|
this.setIdentityLastName,
|
||||||
|
this.setIdentityUserName,
|
||||||
|
this.setIdentityEmail,
|
||||||
|
this.setIdentityPhone,
|
||||||
|
this.setIdentityCompany,
|
||||||
|
];
|
||||||
|
|
||||||
|
abstract setCipherType(value: any, cipher: CipherView): void;
|
||||||
|
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results = this.parseCsv(data, true, {
|
||||||
|
quoteChar: '"',
|
||||||
|
escapeChar: "\\",
|
||||||
|
});
|
||||||
|
if (results == null) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach((value) => {
|
||||||
|
if (this.isNullOrWhitespace(this.getProp(value, "title"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(this.getProp(value, "title"), "--");
|
||||||
|
|
||||||
|
this.setNotes(value, cipher);
|
||||||
|
|
||||||
|
this.setCipherType(value, cipher);
|
||||||
|
|
||||||
|
let altUsername: string = null;
|
||||||
|
for (const property in value) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (!value.hasOwnProperty(property) || this.isNullOrWhitespace(value[property])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = new CipherImportContext(value, property, cipher);
|
||||||
|
if (cipher.type === CipherType.Login && this.setKnownLoginValue(context)) {
|
||||||
|
continue;
|
||||||
|
} else if (cipher.type === CipherType.Card && this.setKnownCreditCardValue(context)) {
|
||||||
|
continue;
|
||||||
|
} else if (cipher.type === CipherType.Identity && this.setKnownIdentityValue(context)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
altUsername = this.setUnknownValue(context, altUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cipher.type === CipherType.Login &&
|
||||||
|
!this.isNullOrWhitespace(altUsername) &&
|
||||||
|
this.isNullOrWhitespace(cipher.login.username) &&
|
||||||
|
altUsername.indexOf("://") === -1
|
||||||
|
) {
|
||||||
|
cipher.login.username = altUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getProp(obj: any, name: string): any {
|
||||||
|
const lowerObj = Object.entries(obj).reduce((agg: any, entry: [string, any]) => {
|
||||||
|
agg[entry[0].toLowerCase()] = entry[1];
|
||||||
|
return agg;
|
||||||
|
}, {});
|
||||||
|
return lowerObj[name.toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropByRegexp(obj: any, regexp: RegExp): any {
|
||||||
|
const matchingKeys = Object.keys(obj).reduce((agg: string[], key: string) => {
|
||||||
|
if (key.match(regexp)) {
|
||||||
|
agg.push(key);
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
}, []);
|
||||||
|
if (matchingKeys.length === 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return obj[matchingKeys[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropIncluding(obj: any, name: string): any {
|
||||||
|
const includesMap = Object.keys(obj).reduce((agg: string[], entry: string) => {
|
||||||
|
if (entry.toLowerCase().includes(name.toLowerCase())) {
|
||||||
|
agg.push(entry);
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
}, []);
|
||||||
|
if (includesMap.length === 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return obj[includesMap[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setNotes(importRecord: any, cipher: CipherView) {
|
||||||
|
cipher.notes =
|
||||||
|
this.getValueOrDefault(this.getProp(importRecord, "notesPlain"), "") +
|
||||||
|
"\n" +
|
||||||
|
this.getValueOrDefault(this.getProp(importRecord, "notes"), "") +
|
||||||
|
"\n";
|
||||||
|
cipher.notes.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setKnownLoginValue(context: CipherImportContext): boolean {
|
||||||
|
return this.loginPropertyParsers.reduce((agg: boolean, func) => {
|
||||||
|
if (!agg) {
|
||||||
|
agg = func.bind(this)(context);
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setKnownCreditCardValue(context: CipherImportContext): boolean {
|
||||||
|
return this.creditCardPropertyParsers.reduce((agg: boolean, func) => {
|
||||||
|
if (!agg) {
|
||||||
|
agg = func.bind(this)(context);
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setKnownIdentityValue(context: CipherImportContext): boolean {
|
||||||
|
return this.identityPropertyParsers.reduce((agg: boolean, func) => {
|
||||||
|
if (!agg) {
|
||||||
|
agg = func.bind(this)(context);
|
||||||
|
}
|
||||||
|
return agg;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setUnknownValue(context: CipherImportContext, altUsername: string): string {
|
||||||
|
if (
|
||||||
|
IgnoredProperties.indexOf(context.lowerProperty) === -1 &&
|
||||||
|
!context.lowerProperty.startsWith("section:") &&
|
||||||
|
!context.lowerProperty.startsWith("section ")
|
||||||
|
) {
|
||||||
|
if (altUsername == null && context.lowerProperty === "email") {
|
||||||
|
return context.importRecord[context.property];
|
||||||
|
} else if (
|
||||||
|
context.lowerProperty === "created date" ||
|
||||||
|
context.lowerProperty === "modified date"
|
||||||
|
) {
|
||||||
|
const readableDate = new Date(
|
||||||
|
parseInt(context.importRecord[context.property], 10) * 1000
|
||||||
|
).toUTCString();
|
||||||
|
this.processKvp(context.cipher, "1Password " + context.property, readableDate);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
context.lowerProperty.includes("password") ||
|
||||||
|
context.lowerProperty.includes("key") ||
|
||||||
|
context.lowerProperty.includes("secret")
|
||||||
|
) {
|
||||||
|
this.processKvp(
|
||||||
|
context.cipher,
|
||||||
|
context.property,
|
||||||
|
context.importRecord[context.property],
|
||||||
|
FieldType.Hidden
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.processKvp(context.cipher, context.property, context.importRecord[context.property]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityFirstName(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.firstName) &&
|
||||||
|
context.lowerProperty.includes("first name")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.firstName = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityInitial(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.middleName) &&
|
||||||
|
context.lowerProperty.includes("initial")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.middleName = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityLastName(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.lastName) &&
|
||||||
|
context.lowerProperty.includes("last name")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.lastName = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityUserName(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.username) &&
|
||||||
|
context.lowerProperty.includes("username")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.username = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityCompany(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.company) &&
|
||||||
|
context.lowerProperty.includes("company")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.company = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityPhone(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.phone) &&
|
||||||
|
context.lowerProperty.includes("default phone")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.phone = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setIdentityEmail(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.identity.email) &&
|
||||||
|
context.lowerProperty.includes("email")
|
||||||
|
) {
|
||||||
|
context.cipher.identity.email = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setCreditCardNumber(context: CipherImportContext): boolean {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.card.number) &&
|
||||||
|
context.lowerProperty.includes("number")
|
||||||
|
) {
|
||||||
|
context.cipher.card.number = context.importRecord[context.property];
|
||||||
|
context.cipher.card.brand = this.getCardBrand(context.cipher.card.number);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setCreditCardVerification(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.card.code) &&
|
||||||
|
context.lowerProperty.includes("verification number")
|
||||||
|
) {
|
||||||
|
context.cipher.card.code = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setCreditCardCardholderName(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.card.cardholderName) &&
|
||||||
|
context.lowerProperty.includes("cardholder name")
|
||||||
|
) {
|
||||||
|
context.cipher.card.cardholderName = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setCreditCardExpiry(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.card.expiration) &&
|
||||||
|
context.lowerProperty.includes("expiry date") &&
|
||||||
|
context.importRecord[context.property].length === 7
|
||||||
|
) {
|
||||||
|
context.cipher.card.expMonth = (context.importRecord[context.property] as string).substr(
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
if (context.cipher.card.expMonth[0] === "0") {
|
||||||
|
context.cipher.card.expMonth = context.cipher.card.expMonth.substr(1, 1);
|
||||||
|
}
|
||||||
|
context.cipher.card.expYear = (context.importRecord[context.property] as string).substr(3, 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setLoginPassword(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.login.password) &&
|
||||||
|
context.lowerProperty === "password"
|
||||||
|
) {
|
||||||
|
context.cipher.login.password = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setLoginUsername(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.login.username) &&
|
||||||
|
context.lowerProperty === "username"
|
||||||
|
) {
|
||||||
|
context.cipher.login.username = context.importRecord[context.property];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setLoginUris(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
(context.cipher.login.uris == null || context.cipher.login.uris.length === 0) &&
|
||||||
|
context.lowerProperty === "urls"
|
||||||
|
) {
|
||||||
|
const urls = context.importRecord[context.property].split(this.newLineRegex);
|
||||||
|
context.cipher.login.uris = this.makeUriArray(urls);
|
||||||
|
return true;
|
||||||
|
} else if (context.lowerProperty === "url") {
|
||||||
|
if (context.cipher.login.uris == null) {
|
||||||
|
context.cipher.login.uris = [];
|
||||||
|
}
|
||||||
|
context.cipher.login.uris.concat(this.makeUriArray(context.importRecord[context.property]));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { CipherType } from "../../enums/cipherType";
|
||||||
|
import { CardView } from "../../models/view/cardView";
|
||||||
|
import { CipherView } from "../../models/view/cipherView";
|
||||||
|
import { IdentityView } from "../../models/view/identityView";
|
||||||
|
import { Importer } from "../importer";
|
||||||
|
|
||||||
|
import { IgnoredProperties, OnePasswordCsvImporter } from "./onepasswordCsvImporter";
|
||||||
|
|
||||||
|
export class OnePasswordMacCsvImporter extends OnePasswordCsvImporter implements Importer {
|
||||||
|
setCipherType(value: any, cipher: CipherView) {
|
||||||
|
const onePassType = this.getValueOrDefault(this.getProp(value, "type"), "Login");
|
||||||
|
switch (onePassType) {
|
||||||
|
case "Credit Card":
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = new CardView();
|
||||||
|
IgnoredProperties.push("type");
|
||||||
|
break;
|
||||||
|
case "Identity":
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
IgnoredProperties.push("type");
|
||||||
|
break;
|
||||||
|
case "Login":
|
||||||
|
case "Secure Note":
|
||||||
|
IgnoredProperties.push("type");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { CipherType } from "../../enums/cipherType";
|
||||||
|
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 { Importer } from "../importer";
|
||||||
|
|
||||||
|
import { CipherImportContext } from "./cipherImportContext";
|
||||||
|
import { OnePasswordCsvImporter } from "./onepasswordCsvImporter";
|
||||||
|
|
||||||
|
export class OnePasswordWinCsvImporter extends OnePasswordCsvImporter implements Importer {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.identityPropertyParsers.push(this.setIdentityAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCipherType(value: any, cipher: CipherView) {
|
||||||
|
cipher.type = CipherType.Login;
|
||||||
|
cipher.login = new LoginView();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.isNullOrWhitespace(this.getPropByRegexp(value, /\d+: number/i)) &&
|
||||||
|
!this.isNullOrWhitespace(this.getPropByRegexp(value, /\d+: expiry date/i))
|
||||||
|
) {
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
cipher.card = new CardView();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.isNullOrWhitespace(this.getPropByRegexp(value, /name \d+: first name/i)) ||
|
||||||
|
!this.isNullOrWhitespace(this.getPropByRegexp(value, /name \d+: initial/i)) ||
|
||||||
|
!this.isNullOrWhitespace(this.getPropByRegexp(value, /name \d+: last name/i)) ||
|
||||||
|
!this.isNullOrWhitespace(this.getPropByRegexp(value, /internet \d+: email/i))
|
||||||
|
) {
|
||||||
|
cipher.type = CipherType.Identity;
|
||||||
|
cipher.identity = new IdentityView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIdentityAddress(context: CipherImportContext) {
|
||||||
|
if (context.lowerProperty.match(/address \d+: address/i)) {
|
||||||
|
this.processKvp(context.cipher, "address", context.importRecord[context.property]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreditCardExpiry(context: CipherImportContext) {
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(context.cipher.card.expiration) &&
|
||||||
|
context.lowerProperty.includes("expiry date")
|
||||||
|
) {
|
||||||
|
const expSplit = (context.importRecord[context.property] as string).split("/");
|
||||||
|
context.cipher.card.expMonth = expSplit[0];
|
||||||
|
if (context.cipher.card.expMonth[0] === "0" && context.cipher.card.expMonth.length === 2) {
|
||||||
|
context.cipher.card.expMonth = context.cipher.card.expMonth.substr(1, 1);
|
||||||
|
}
|
||||||
|
context.cipher.card.expYear = expSplit[2].length > 4 ? expSplit[2].substr(0, 4) : expSplit[2];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
export interface ExportData {
|
||||||
|
accounts?: AccountsEntity[] | null;
|
||||||
|
}
|
||||||
|
export interface AccountsEntity {
|
||||||
|
attrs: AccountAttributes;
|
||||||
|
vaults?: VaultsEntity[] | null;
|
||||||
|
}
|
||||||
|
export interface AccountAttributes {
|
||||||
|
accountName: string;
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
email: string;
|
||||||
|
uuid: string;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
export interface VaultsEntity {
|
||||||
|
attrs: VaultAttributes;
|
||||||
|
items?: Item[] | null;
|
||||||
|
}
|
||||||
|
export interface VaultAttributes {
|
||||||
|
uuid: string;
|
||||||
|
desc: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CategoryEnum {
|
||||||
|
Login = "001",
|
||||||
|
CreditCard = "002",
|
||||||
|
SecureNote = "003",
|
||||||
|
Identity = "004",
|
||||||
|
Password = "005",
|
||||||
|
Document = "006",
|
||||||
|
SoftwareLicense = "100",
|
||||||
|
BankAccount = "101",
|
||||||
|
Database = "102",
|
||||||
|
DriversLicense = "103",
|
||||||
|
OutdoorLicense = "104",
|
||||||
|
Membership = "105",
|
||||||
|
Passport = "106",
|
||||||
|
RewardsProgram = "107",
|
||||||
|
SocialSecurityNumber = "108",
|
||||||
|
WirelessRouter = "109",
|
||||||
|
Server = "110",
|
||||||
|
EmailAccount = "111",
|
||||||
|
API_Credential = "112",
|
||||||
|
MedicalRecord = "113",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
uuid: string;
|
||||||
|
favIndex: number;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
trashed?: boolean;
|
||||||
|
categoryUuid: string;
|
||||||
|
details: Details;
|
||||||
|
overview: Overview;
|
||||||
|
}
|
||||||
|
export interface Details {
|
||||||
|
loginFields?: (LoginFieldsEntity | null)[] | null;
|
||||||
|
notesPlain?: string | null;
|
||||||
|
sections?: (SectionsEntity | null)[] | null;
|
||||||
|
passwordHistory?: (PasswordHistoryEntity | null)[] | null;
|
||||||
|
documentAttributes?: DocumentAttributes | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LoginFieldTypeEnum {
|
||||||
|
TextOrHtml = "T",
|
||||||
|
EmailAddress = "E",
|
||||||
|
URL = "U",
|
||||||
|
Number = "N",
|
||||||
|
Password = "P",
|
||||||
|
TextArea = "A",
|
||||||
|
PhoneNumber = "T",
|
||||||
|
CheckBox = "C",
|
||||||
|
}
|
||||||
|
export interface LoginFieldsEntity {
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
fieldType: LoginFieldTypeEnum | string;
|
||||||
|
designation?: string | null;
|
||||||
|
}
|
||||||
|
export interface SectionsEntity {
|
||||||
|
title: string;
|
||||||
|
name?: string | null;
|
||||||
|
fields?: FieldsEntity[] | null;
|
||||||
|
}
|
||||||
|
export interface FieldsEntity {
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
value: Value;
|
||||||
|
indexAtSource: number;
|
||||||
|
guarded: boolean;
|
||||||
|
multiline: boolean;
|
||||||
|
dontGenerate: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
inputTraits: InputTraits;
|
||||||
|
clipboardFilter?: string | null;
|
||||||
|
}
|
||||||
|
export interface Value {
|
||||||
|
totp?: string | null;
|
||||||
|
date?: number | null;
|
||||||
|
string?: string | null;
|
||||||
|
concealed?: string | null;
|
||||||
|
email?: Email | null;
|
||||||
|
phone?: string | null;
|
||||||
|
menu?: string | null;
|
||||||
|
gender?: string | null;
|
||||||
|
monthYear?: number | null;
|
||||||
|
url?: string | null;
|
||||||
|
address?: Address | null;
|
||||||
|
creditCardType?: string | null;
|
||||||
|
creditCardNumber?: string | null;
|
||||||
|
reference?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Email {
|
||||||
|
email_address: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
country: string;
|
||||||
|
zip: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
export interface InputTraits {
|
||||||
|
keyboard: string;
|
||||||
|
correction: string;
|
||||||
|
capitalization: string;
|
||||||
|
}
|
||||||
|
export interface PasswordHistoryEntity {
|
||||||
|
value: string;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
export interface DocumentAttributes {
|
||||||
|
fileName: string;
|
||||||
|
documentId: string;
|
||||||
|
decryptedSize: number;
|
||||||
|
}
|
||||||
|
export interface Overview {
|
||||||
|
subtitle: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
urls?: UrlsEntity[] | null;
|
||||||
|
ps?: number | null;
|
||||||
|
pbe?: number | null;
|
||||||
|
pgrng?: boolean | null;
|
||||||
|
tags?: string[] | null;
|
||||||
|
}
|
||||||
|
export interface UrlsEntity {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
85
jslib/common/src/importers/padlockCsvImporter.ts
Normal file
85
jslib/common/src/importers/padlockCsvImporter.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { CollectionView } from "../models/view/collectionView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PadlockCsvImporter 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 headers: string[] = null;
|
||||||
|
results.forEach((value) => {
|
||||||
|
if (headers == null) {
|
||||||
|
headers = value.map((v: string) => v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 2 || value.length !== headers.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(value[1])) {
|
||||||
|
if (this.organization) {
|
||||||
|
const tags = (value[1] as string).split(",");
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
tag = tag.trim();
|
||||||
|
let addCollection = true;
|
||||||
|
let collectionIndex = result.collections.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < result.collections.length; i++) {
|
||||||
|
if (result.collections[i].name === tag) {
|
||||||
|
addCollection = false;
|
||||||
|
collectionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addCollection) {
|
||||||
|
const collection = new CollectionView();
|
||||||
|
collection.name = tag;
|
||||||
|
result.collections.push(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.collectionRelationships.push([result.ciphers.length, collectionIndex]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const tags = (value[1] as string).split(",");
|
||||||
|
const tag = tags.length > 0 ? tags[0].trim() : null;
|
||||||
|
this.processFolder(result, tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(value[0], "--");
|
||||||
|
|
||||||
|
for (let i = 2; i < value.length; i++) {
|
||||||
|
const header = headers[i].trim().toLowerCase();
|
||||||
|
if (this.isNullOrWhitespace(value[i]) || this.isNullOrWhitespace(header)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.usernameFieldNames.indexOf(header) > -1) {
|
||||||
|
cipher.login.username = value[i];
|
||||||
|
} else if (this.passwordFieldNames.indexOf(header) > -1) {
|
||||||
|
cipher.login.password = value[i];
|
||||||
|
} else if (this.uriFieldNames.indexOf(header) > -1) {
|
||||||
|
cipher.login.uris = this.makeUriArray(value[i]);
|
||||||
|
} else {
|
||||||
|
this.processKvp(cipher, headers[i], value[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
jslib/common/src/importers/passkeepCsvImporter.ts
Normal file
39
jslib/common/src/importers/passkeepCsvImporter.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PassKeepCsvImporter 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.getValue("category", value));
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.notes = this.getValue("description", value);
|
||||||
|
cipher.name = this.getValueOrDefault(this.getValue("title", value), "--");
|
||||||
|
cipher.login.username = this.getValue("username", value);
|
||||||
|
cipher.login.password = this.getValue("password", value);
|
||||||
|
cipher.login.uris = this.makeUriArray(this.getValue("site", value));
|
||||||
|
this.processKvp(cipher, "Password 2", this.getValue("password2", value));
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getValue(key: string, value: any) {
|
||||||
|
return this.getValueOrDefault(value[key], this.getValueOrDefault(value[" " + key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
61
jslib/common/src/importers/passmanJsonImporter.ts
Normal file
61
jslib/common/src/importers/passmanJsonImporter.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PassmanJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results = JSON.parse(data);
|
||||||
|
if (results == null || results.length === 0) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach((credential: any) => {
|
||||||
|
if (credential.tags != null && credential.tags.length > 0) {
|
||||||
|
const folderName = credential.tags[0].text;
|
||||||
|
this.processFolder(result, folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = credential.label;
|
||||||
|
|
||||||
|
cipher.login.username = this.getValueOrDefault(credential.username);
|
||||||
|
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.url);
|
||||||
|
cipher.notes += this.getValueOrDefault(credential.description, "");
|
||||||
|
if (credential.otp != null) {
|
||||||
|
cipher.login.totp = this.getValueOrDefault(credential.otp.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.custom_fields != null) {
|
||||||
|
credential.custom_fields.forEach((customField: any) => {
|
||||||
|
switch (customField.field_type) {
|
||||||
|
case "text":
|
||||||
|
case "password":
|
||||||
|
this.processKvp(cipher, customField.label, customField.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
jslib/common/src/importers/passpackCsvImporter.ts
Normal file
103
jslib/common/src/importers/passpackCsvImporter.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { CollectionView } from "../models/view/collectionView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PasspackCsvImporter 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 tagsJson = !this.isNullOrWhitespace(value.Tags) ? JSON.parse(value.Tags) : null;
|
||||||
|
const tags: string[] =
|
||||||
|
tagsJson != null && tagsJson.tags != null && tagsJson.tags.length > 0
|
||||||
|
? tagsJson.tags
|
||||||
|
.map((tagJson: string) => {
|
||||||
|
try {
|
||||||
|
const t = JSON.parse(tagJson);
|
||||||
|
return this.getValueOrDefault(t.tag);
|
||||||
|
} catch {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((t: string) => !this.isNullOrWhitespace(t))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (this.organization && tags != null && tags.length > 0) {
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
let addCollection = true;
|
||||||
|
let collectionIndex = result.collections.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < result.collections.length; i++) {
|
||||||
|
if (result.collections[i].name === tag) {
|
||||||
|
addCollection = false;
|
||||||
|
collectionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addCollection) {
|
||||||
|
const collection = new CollectionView();
|
||||||
|
collection.name = tag;
|
||||||
|
result.collections.push(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.collectionRelationships.push([result.ciphers.length, collectionIndex]);
|
||||||
|
});
|
||||||
|
} else if (!this.organization && tags != null && tags.length > 0) {
|
||||||
|
this.processFolder(result, tags[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.notes = this.getValueOrDefault(value.Notes, "");
|
||||||
|
cipher.notes += "\n\n" + this.getValueOrDefault(value["Shared Notes"], "") + "\n";
|
||||||
|
cipher.name = this.getValueOrDefault(value["Entry Name"], "--");
|
||||||
|
cipher.login.username = this.getValueOrDefault(value["User ID"]);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value.Password);
|
||||||
|
cipher.login.uris = this.makeUriArray(value.URL);
|
||||||
|
|
||||||
|
if (value.__parsed_extra != null && value.__parsed_extra.length > 0) {
|
||||||
|
value.__parsed_extra.forEach((extra: string) => {
|
||||||
|
if (!this.isNullOrWhitespace(extra)) {
|
||||||
|
cipher.notes += "\n" + extra;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsJson = !this.isNullOrWhitespace(value["Extra Fields"])
|
||||||
|
? JSON.parse(value["Extra Fields"])
|
||||||
|
: null;
|
||||||
|
const fields =
|
||||||
|
fieldsJson != null && fieldsJson.extraFields != null && fieldsJson.extraFields.length > 0
|
||||||
|
? fieldsJson.extraFields.map((fieldJson: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fieldJson);
|
||||||
|
} catch {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (fields != null) {
|
||||||
|
fields.forEach((f: any) => {
|
||||||
|
if (f != null) {
|
||||||
|
this.processKvp(cipher, f.name, f.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
jslib/common/src/importers/passwordAgentCsvImporter.ts
Normal file
52
jslib/common/src/importers/passwordAgentCsvImporter.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PasswordAgentCsvImporter 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 newVersion = true;
|
||||||
|
results.forEach((value) => {
|
||||||
|
if (value.length !== 5 && value.length < 9) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const altFormat = value.length === 10 && value[0] === "0";
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(value[altFormat ? 1 : 0], "--");
|
||||||
|
cipher.login.username = this.getValueOrDefault(value[altFormat ? 2 : 1]);
|
||||||
|
cipher.login.password = this.getValueOrDefault(value[altFormat ? 3 : 2]);
|
||||||
|
if (value.length === 5) {
|
||||||
|
newVersion = false;
|
||||||
|
cipher.notes = this.getValueOrDefault(value[4]);
|
||||||
|
cipher.login.uris = this.makeUriArray(value[3]);
|
||||||
|
} else {
|
||||||
|
const folder = this.getValueOrDefault(value[altFormat ? 9 : 8], "(None)");
|
||||||
|
let folderName = folder !== "(None)" ? folder.split("\\").join("/") : null;
|
||||||
|
if (folderName != null) {
|
||||||
|
folderName = folder.split(" > ").join("/");
|
||||||
|
folderName = folder.split(">").join("/");
|
||||||
|
}
|
||||||
|
this.processFolder(result, folderName);
|
||||||
|
cipher.notes = this.getValueOrDefault(value[altFormat ? 5 : 3]);
|
||||||
|
cipher.login.uris = this.makeUriArray(value[4]);
|
||||||
|
}
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newVersion && this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
jslib/common/src/importers/passwordBossJsonImporter.ts
Normal file
130
jslib/common/src/importers/passwordBossJsonImporter.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { CipherType } from "../enums/cipherType";
|
||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
import { CardView } from "../models/view/cardView";
|
||||||
|
import { FolderView } from "../models/view/folderView";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PasswordBossJsonImporter extends BaseImporter implements Importer {
|
||||||
|
parse(data: string): Promise<ImportResult> {
|
||||||
|
const result = new ImportResult();
|
||||||
|
const results = JSON.parse(data);
|
||||||
|
if (results == null || results.items == null) {
|
||||||
|
result.success = false;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foldersMap = new Map<string, string>();
|
||||||
|
results.folders.forEach((value: any) => {
|
||||||
|
foldersMap.set(value.id, value.name);
|
||||||
|
});
|
||||||
|
const foldersIndexMap = new Map<string, number>();
|
||||||
|
foldersMap.forEach((val, key) => {
|
||||||
|
foldersIndexMap.set(key, result.folders.length);
|
||||||
|
const f = new FolderView();
|
||||||
|
f.name = val;
|
||||||
|
result.folders.push(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
results.items.forEach((value: any) => {
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name = this.getValueOrDefault(value.name, "--");
|
||||||
|
cipher.login.uris = this.makeUriArray(value.login_url);
|
||||||
|
|
||||||
|
if (value.folder != null && foldersIndexMap.has(value.folder)) {
|
||||||
|
result.folderRelationships.push([result.ciphers.length, foldersIndexMap.get(value.folder)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.identifiers == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isNullOrWhitespace(value.identifiers.notes)) {
|
||||||
|
cipher.notes = value.identifiers.notes.split("\\r\\n").join("\n").split("\\n").join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.type === "CreditCard") {
|
||||||
|
cipher.card = new CardView();
|
||||||
|
cipher.type = CipherType.Card;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const property in value.identifiers) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (!value.identifiers.hasOwnProperty(property)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const valObj = value.identifiers[property];
|
||||||
|
const val = valObj != null ? valObj.toString() : null;
|
||||||
|
if (
|
||||||
|
this.isNullOrWhitespace(val) ||
|
||||||
|
property === "notes" ||
|
||||||
|
property === "ignoreItemInSecurityScore"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property === "custom_fields") {
|
||||||
|
valObj.forEach((cf: any) => {
|
||||||
|
this.processKvp(cipher, cf.name, cf.value);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipher.type === CipherType.Card) {
|
||||||
|
if (property === "cardNumber") {
|
||||||
|
cipher.card.number = val;
|
||||||
|
cipher.card.brand = this.getCardBrand(val);
|
||||||
|
continue;
|
||||||
|
} else if (property === "nameOnCard") {
|
||||||
|
cipher.card.cardholderName = val;
|
||||||
|
continue;
|
||||||
|
} else if (property === "security_code") {
|
||||||
|
cipher.card.code = val;
|
||||||
|
continue;
|
||||||
|
} else if (property === "expires") {
|
||||||
|
try {
|
||||||
|
const expDate = new Date(val);
|
||||||
|
cipher.card.expYear = expDate.getFullYear().toString();
|
||||||
|
cipher.card.expMonth = (expDate.getMonth() + 1).toString();
|
||||||
|
} catch {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else if (property === "cardType") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
(property === "username" || property === "email") &&
|
||||||
|
this.isNullOrWhitespace(cipher.login.username)
|
||||||
|
) {
|
||||||
|
cipher.login.username = val;
|
||||||
|
continue;
|
||||||
|
} else if (property === "password") {
|
||||||
|
cipher.login.password = val;
|
||||||
|
continue;
|
||||||
|
} else if (property === "totp") {
|
||||||
|
cipher.login.totp = val;
|
||||||
|
continue;
|
||||||
|
} else if (
|
||||||
|
(cipher.login.uris == null || cipher.login.uris.length === 0) &&
|
||||||
|
this.uriFieldNames.indexOf(property) > -1
|
||||||
|
) {
|
||||||
|
cipher.login.uris = this.makeUriArray(val);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processKvp(cipher, property, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertToNoteIfNeeded(cipher);
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
jslib/common/src/importers/passwordDragonXmlImporter.ts
Normal file
63
jslib/common/src/importers/passwordDragonXmlImporter.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { ImportResult } from "../models/domain/importResult";
|
||||||
|
|
||||||
|
import { BaseImporter } from "./baseImporter";
|
||||||
|
import { Importer } from "./importer";
|
||||||
|
|
||||||
|
export class PasswordDragonXmlImporter 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 records = doc.querySelectorAll("PasswordManager > record");
|
||||||
|
Array.from(records).forEach((record) => {
|
||||||
|
const category = this.querySelectorDirectChild(record, "Category");
|
||||||
|
const categoryText =
|
||||||
|
category != null &&
|
||||||
|
!this.isNullOrWhitespace(category.textContent) &&
|
||||||
|
category.textContent !== "Unfiled"
|
||||||
|
? category.textContent
|
||||||
|
: null;
|
||||||
|
this.processFolder(result, categoryText);
|
||||||
|
|
||||||
|
const accountName = this.querySelectorDirectChild(record, "Account-Name");
|
||||||
|
const userId = this.querySelectorDirectChild(record, "User-Id");
|
||||||
|
const password = this.querySelectorDirectChild(record, "Password");
|
||||||
|
const url = this.querySelectorDirectChild(record, "URL");
|
||||||
|
const notes = this.querySelectorDirectChild(record, "Notes");
|
||||||
|
const cipher = this.initLoginCipher();
|
||||||
|
cipher.name =
|
||||||
|
accountName != null ? this.getValueOrDefault(accountName.textContent, "--") : "--";
|
||||||
|
cipher.notes = notes != null ? this.getValueOrDefault(notes.textContent) : "";
|
||||||
|
cipher.login.username = userId != null ? this.getValueOrDefault(userId.textContent) : null;
|
||||||
|
cipher.login.password =
|
||||||
|
password != null ? this.getValueOrDefault(password.textContent) : null;
|
||||||
|
cipher.login.uris = url != null ? this.makeUriArray(url.textContent) : null;
|
||||||
|
|
||||||
|
const attributes: string[] = [];
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
attributes.push("Attribute-" + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.querySelectorAllDirectChild(record, attributes.join(",")).forEach((attr) => {
|
||||||
|
if (this.isNullOrWhitespace(attr.textContent) || attr.textContent === "null") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processKvp(cipher, attr.tagName, attr.textContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cleanupCipher(cipher);
|
||||||
|
result.ciphers.push(cipher);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.organization) {
|
||||||
|
this.moveFoldersToCollections(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = true;
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user