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

Compare commits

..

35 Commits

Author SHA1 Message Date
Addison Beck
83c521987e Upgrade to Angular 17 2023-12-07 00:18:31 +00:00
Addison Beck
cd71a7a0ae Upgrade angular cdk 2023-12-07 00:11:00 +00:00
Addison Beck
c0bdb67ce9 Upgrade typescript and zone in preperation for Angular 17 2023-12-06 23:37:17 +00:00
Addison Beck
ae8ce6643d Upgrade to Angular 16 2023-12-06 23:15:48 +00:00
Addison Beck
9e131dc9c8 Upgrade a few majors for safe dev deps 2023-12-06 21:34:55 +00:00
Addison Beck
ebe5eaedb2 Applied all minor updates 2023-12-06 20:17:50 +00:00
Addison Beck
a098620b95 Applied all patch updates 2023-12-06 20:14:54 +00:00
Addison Beck
d222a26eb6 Rebuild package-lock.json 2023-12-06 20:06:54 +00:00
Addison Beck
d50e10bb55 Remove angular eslint plugin 2023-12-06 19:33:41 +00:00
Addison Beck
aca1daa58f Resolve remaining eslint errors 2023-12-06 18:00:44 +00:00
Addison Beck
5bd63c6d90 Fix eslint issues related to state 2023-12-06 17:52:45 +00:00
Addison Beck
f71f6db0e5 Fix eslint issues in scripts 2023-12-06 17:52:31 +00:00
Addison Beck
8ef50234c8 Autofixed most eslint concerns 2023-12-06 00:25:52 +00:00
Addison Beck
caa846f065 Fixed eslint 2023-12-06 00:10:46 +00:00
Addison Beck
6c553a020c Make the angular cli a dev dependency 2023-12-05 23:38:27 +00:00
Addison Beck
3c2caf0ef5 Removed subsitute 2023-12-05 22:22:57 +00:00
Addison Beck
74b7287ba7 Removed tests 2023-12-05 22:18:22 +00:00
Addison
48ee5eb4da Replace jsdom with jest-environment-jsdom 2023-11-29 22:34:36 +00:00
Addison
ce80c031db Removed nodemon 2023-11-29 21:45:48 +00:00
Addison
0ec615e16b Disable husky in CI 2023-11-29 21:25:33 +00:00
Addison
3a2f0988a5 Remove prebuild-install 2023-11-29 21:25:33 +00:00
Addison
de4431a559 tapable 2023-11-29 21:25:32 +00:00
Addison
928785493b Remove ts-node 2023-11-29 21:25:32 +00:00
Addison
df696ec9d3 Removed ttypescript 2023-11-29 21:25:31 +00:00
Addison
0debaf1237 Remove tsconfig-paths 2023-11-29 21:25:31 +00:00
Addison
23f4ef0bd1 Remove typescript-transform-paths 2023-11-29 21:25:30 +00:00
Addison
c8d9a6aed9 Removed typemoq 2023-11-29 21:25:30 +00:00
Addison
92fb2be27c Rename webpack config files 2023-11-29 21:25:29 +00:00
Addison
3dc66d0a9a Rebuild package-lock.json 2023-11-29 21:25:29 +00:00
Addison
b10ee28bfe Explicitly add ts-jest to package.json 2023-11-29 21:25:28 +00:00
Addison
b3d25a9615 Explicitly add dotenv to package.json 2023-11-29 21:25:28 +00:00
Addison
9392e513a0 Explicitly add google-auth-library to package.json 2023-11-29 21:25:27 +00:00
Addison
57c55d7f3c Adjust imports for /src/ 2023-11-29 21:25:27 +00:00
Addison
7d04a0d1eb Adjust .tsconfig path for root and apply to jslib 2023-11-29 21:25:26 +00:00
Addison
9b043c122f Unpackage-ify jslib 2023-11-29 21:25:26 +00:00
197 changed files with 17742 additions and 10640 deletions

View File

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

View File

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

View File

@@ -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
View File

@@ -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"
}
]
}

View File

@@ -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 }}

View File

@@ -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' }}

View File

@@ -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

View File

@@ -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 }}"

View File

@@ -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

View File

@@ -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,
},
],
},
};

View File

@@ -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,
});

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(

View File

@@ -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.

View File

@@ -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,

View File

@@ -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),
});
});
});

View File

@@ -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",
});
});
});

View File

@@ -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,
});
});
});
});

View File

@@ -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,
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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,
});
});
});

View File

@@ -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"),
});
});
});

View File

@@ -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",
});
});
});

View File

@@ -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);
});
});

View File

@@ -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,
});
});
});

View File

@@ -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"),
});
});
});

View File

@@ -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,
});
});
});

View File

@@ -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,
});
});
});

View File

@@ -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",
});
});
});

View File

@@ -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",
});
});
});

View File

@@ -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,
});
});
});

View File

@@ -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.");
});
});
});

View File

@@ -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);
});
});

View File

@@ -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
);
})
);
});
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
});
});

View File

@@ -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({});
});
});

View File

@@ -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()
);
});
});
});

View File

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

View File

@@ -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;
}

View 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;
}

View 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;
}

View 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"];

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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(" ");
}
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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
);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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]);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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;
}

View 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);
}
}

View 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
);
}
}

View 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);
}
}
});
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -0,0 +1,5 @@
export class ImportError extends Error {
constructor(message?: string, public passwordRequired: boolean = false) {
super(message);
}
}

View File

@@ -0,0 +1,6 @@
import { ImportResult } from "../models/domain/importResult";
export interface Importer {
organizationId: string;
parse(data: string): Promise<ImportResult>;
}

View 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;
}
}

View 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);
});
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
});
}
}

View File

@@ -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;
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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]);
});
}
}

View 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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);
}
}

View 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]));
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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