mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-15 15:53:41 +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
|
||||
build
|
||||
build-cli
|
||||
webpack.cli.js
|
||||
webpack.main.js
|
||||
webpack.renderer.js
|
||||
**/webpack**.config.js
|
||||
|
||||
**/node_modules
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"webextensions": true,
|
||||
"node": true
|
||||
},
|
||||
"overrides": [
|
||||
@@ -10,7 +11,7 @@
|
||||
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.eslint.json"],
|
||||
"project": ["./tsconfig.json"],
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
@@ -52,12 +53,12 @@
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@/jslib/**/*",
|
||||
"pattern": "@bitwarden/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "@/src/**/*",
|
||||
"pattern": "src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
@@ -85,11 +86,7 @@
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"parser": "@angular-eslint/template-parser",
|
||||
"plugins": ["@angular-eslint/template"],
|
||||
"rules": {
|
||||
"@angular-eslint/template/button-has-type": "error"
|
||||
}
|
||||
"parser": "@angular-eslint/template-parser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
28
.github/renovate.json
vendored
28
.github/renovate.json
vendored
@@ -2,30 +2,40 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"github>bitwarden/renovate-config:pin-actions",
|
||||
":combinePatchMinorReleases",
|
||||
":dependencyDashboard",
|
||||
":maintainLockFilesWeekly",
|
||||
":pinAllExceptPeerDependencies",
|
||||
":prConcurrentLimit10",
|
||||
":rebaseStalePrs",
|
||||
":separateMajorReleases",
|
||||
"group:monorepos",
|
||||
"schedule:weekends"
|
||||
"schedule:weekends",
|
||||
":separateMajorReleases"
|
||||
],
|
||||
"enabledManagers": ["github-actions", "npm"],
|
||||
"commitMessagePrefix": "[deps]:",
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "gh minor",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"groupName": "npm minor",
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["package.json"],
|
||||
"description": "Admin Console owns general dependencies",
|
||||
"reviewers": ["team:team-admin-console-dev"]
|
||||
"packageNames": ["typescript"],
|
||||
"matchUpdateTypes": ["major", "minor"],
|
||||
"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
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
paths-ignore:
|
||||
- '.github/workflows/**'
|
||||
workflow_dispatch: {}
|
||||
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
@@ -44,7 +49,7 @@ jobs:
|
||||
needs: setup
|
||||
env:
|
||||
_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
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -55,7 +60,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '20.11.0'
|
||||
node-version: '18'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -139,7 +144,7 @@ jobs:
|
||||
needs: setup
|
||||
env:
|
||||
_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
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '20.11.0'
|
||||
node-version: '18'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -226,7 +231,7 @@ jobs:
|
||||
needs: setup
|
||||
env:
|
||||
_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
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -242,7 +247,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '20.11.0'
|
||||
node-version: '18'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -332,7 +337,8 @@ jobs:
|
||||
|
||||
- name: Zip
|
||||
shell: cmd
|
||||
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
|
||||
run: |
|
||||
7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
|
||||
|
||||
- name: Version Test
|
||||
shell: pwsh
|
||||
@@ -382,7 +388,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '20.11.0'
|
||||
node-version: '18'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -456,7 +462,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '20.11.0'
|
||||
node-version: '18'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -510,7 +516,7 @@ jobs:
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '20.11.0'
|
||||
node-version: '18'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
@@ -580,7 +586,6 @@ jobs:
|
||||
env:
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
- name: Upload .zip artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
@@ -625,7 +630,7 @@ jobs:
|
||||
- macos-gui
|
||||
steps:
|
||||
- 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:
|
||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: main
|
||||
branch: master
|
||||
|
||||
- name: Create release
|
||||
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
|
||||
run-name: Version Bump - v${{ inputs.version_number }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New version (example: '2024.1.0')"
|
||||
description: "New Version"
|
||||
required: true
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
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
|
||||
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
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
@@ -29,15 +26,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key,
|
||||
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
|
||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
@@ -48,74 +37,39 @@ jobs:
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Create Version Branch
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Verify input version
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
run: |
|
||||
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: Checkout Version Branch
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
with:
|
||||
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Bump Version - Package
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./package.json"
|
||||
|
||||
- name: Setup git
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
|
||||
- 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
|
||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
run: git push -u origin $PR_BRANCH
|
||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Create Version PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
id: create-pr
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
BASE_BRANCH: master
|
||||
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||
run: |
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
gh pr create --title "$TITLE" \
|
||||
--base "$BASE" \
|
||||
--head "$PR_BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
@@ -128,42 +82,4 @@ jobs:
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ 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
|
||||
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||
|
||||
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
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 { ModalRef } from "./modal/modal.ref";
|
||||
|
||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
|
||||
import { ModalRef } from "./modal/modal.ref";
|
||||
|
||||
/**
|
||||
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
|
||||
|
||||
import { ValidationService } from "../services/validation.service";
|
||||
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
|
||||
|
||||
import { ValidationService } from "../services/validation.service";
|
||||
|
||||
/**
|
||||
* Provides error handling, in particular for any error returned by the server in an api call.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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";
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuardService implements CanActivate {
|
||||
export class AuthGuardService {
|
||||
constructor(
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private router: Router,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
|
||||
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
import { BroadcasterService } from "./broadcaster.service";
|
||||
import { LockGuardService } from "./lock-guard.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||
import { UnauthGuardService } from "./unauth-guard.service";
|
||||
import { ValidationService } from "./validation.service";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@/jslib/common/src/abstractions/audit.service";
|
||||
@@ -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 { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.service";
|
||||
import { ExportService as ExportServiceAbstraction } from "@/jslib/common/src/abstractions/export.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@/jslib/common/src/abstractions/fileUpload.service";
|
||||
import { FolderService as FolderServiceAbstraction } from "@/jslib/common/src/abstractions/folder.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
@@ -50,6 +59,7 @@ import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.servic
|
||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
import { EventService } from "@/jslib/common/src/services/event.service";
|
||||
import { ExportService } from "@/jslib/common/src/services/export.service";
|
||||
import { FileUploadService } from "@/jslib/common/src/services/fileUpload.service";
|
||||
import { FolderService } from "@/jslib/common/src/services/folder.service";
|
||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
||||
@@ -70,14 +80,8 @@ import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service"
|
||||
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
|
||||
import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service";
|
||||
import { VaultTimeoutService } from "@/jslib/common/src/services/vaultTimeout.service";
|
||||
import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoFunction.service";
|
||||
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
import { BroadcasterService } from "./broadcaster.service";
|
||||
import { LockGuardService } from "./lock-guard.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||
import { UnauthGuardService } from "./unauth-guard.service";
|
||||
import { ValidationService } from "./validation.service";
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
@@ -369,6 +373,16 @@ import { ValidationService } from "./validation.service";
|
||||
),
|
||||
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||
},
|
||||
{
|
||||
provide: ExportServiceAbstraction,
|
||||
useClass: ExportService,
|
||||
deps: [
|
||||
FolderServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: SearchServiceAbstraction,
|
||||
useClass: SearchService,
|
||||
@@ -407,6 +421,11 @@ import { ValidationService } from "./validation.service";
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: CryptoFunctionServiceAbstraction,
|
||||
useClass: WebCryptoFunctionService,
|
||||
deps: ["WINDOW"],
|
||||
},
|
||||
{
|
||||
provide: EventServiceAbstraction,
|
||||
useClass: EventService,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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 { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||
|
||||
@Injectable()
|
||||
export class LockGuardService implements CanActivate {
|
||||
export class LockGuardService {
|
||||
protected homepage = "vault";
|
||||
protected loginpage = "login";
|
||||
constructor(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
|
||||
|
||||
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||
|
||||
import { ModalService } from "./modal.service";
|
||||
|
||||
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.
|
||||
* 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 { CanActivate, Router } from "@angular/router";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
|
||||
|
||||
@Injectable()
|
||||
export class UnauthGuardService implements CanActivate {
|
||||
export class UnauthGuardService {
|
||||
protected homepage = "vault";
|
||||
constructor(
|
||||
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