Merge remote-tracking branch 'cli/mono-repo-prep' into cli

# Conflicts:
#	.gitmodules
This commit is contained in:
Hinton 2022-05-25 11:02:23 +02:00
commit 653619b560
103 changed files with 21386 additions and 0 deletions

5
.gitmodules vendored
View File

@ -7,3 +7,8 @@
path = apps/desktop/jslib
url = https://github.com/bitwarden/jslib.git
branch = master
[submodule "apps/cli/jslib"]
path = apps/cli/jslib
url = https://github.com/bitwarden/jslib.git
branch = master

15
apps/cli/.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Set default charset
[*.{js,ts,scss,html}]
charset = utf-8
indent_style = space
indent_size = 2

6
apps/cli/.eslintignore Normal file
View File

@ -0,0 +1,6 @@
dist
build
jslib
webpack.config.js
**/node_modules

31
apps/cli/.eslintrc.json Normal file
View File

@ -0,0 +1,31 @@
{
"root": true,
"env": {
"node": true
},
"extends": ["./jslib/shared/eslintrc.json"],
"rules": {
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "jslib-*/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
]
}
}

View File

@ -0,0 +1,5 @@
# Apply Prettier https://github.com/bitwarden/cli/pull/426
910b4a24e649f21acbf4da5b2d422b121d514bd5
# jslib Apply Prettier https://github.com/bitwarden/jslib/pull/581
193434461dbd9c48fe5dcbad95693470aec422ac

1
apps/cli/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -0,0 +1,28 @@
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->
- **file.ext:** Description of what was changed and why
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

389
apps/cli/.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,389 @@
---
name: Build
on:
push:
branches-ignore:
- 'l10n_master'
paths-ignore:
- '.github/workflows/**'
workflow_dispatch:
inputs: {}
jobs:
cloc:
name: CLOC
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up cloc
run: |
sudo apt update
sudo apt -y install cloc
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript --vcs git
setup:
name: Setup
runs-on: ubuntu-20.04
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Get Package Version
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version package.json)
echo "::set-output name=package_version::$PKG_VERSION"
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Cache npm
id: npm-cache
uses: actions/cache@937d24475381cd9c75ae6db12cb4e79714b926ed
with:
path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Setup sub-module
run: npm run sub:init
- name: Install
run: npm ci
- name: Run linter
run: npm run lint
cli:
name: Build CLI
runs-on: windows-2019
needs:
- setup
- lint
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 16.14.2
_WIN_PKG_VERSION: 3.3
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '16'
- name: Get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v$env:_WIN_PKG_VERSION/node-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
New-Item -ItemType directory -Path .\.pkg-cache
New-Item -ItemType directory -Path .\.pkg-cache\v$env:_WIN_PKG_VERSION
Invoke-RestMethod -Uri $fetchedUrl `
-OutFile ".\.pkg-cache\v$env:_WIN_PKG_VERSION\fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
- name: Setup Version Info
shell: pwsh
run: |
$major,$minor,$patch = $env:_PACKAGE_VERSION.split('.')
$versionInfo = @"
1 VERSIONINFO
FILEVERSION $major,$minor,$patch,0
PRODUCTVERSION $major,$minor,$patch,0
FILEOS 0x40004
FILETYPE 0x1
{
BLOCK "StringFileInfo"
{
BLOCK "040904b0"
{
VALUE "CompanyName", "Bitwarden Inc."
VALUE "ProductName", "Bitwarden"
VALUE "FileDescription", "Bitwarden CLI"
VALUE "FileVersion", "$env:_PACKAGE_VERSION"
VALUE "ProductVersion", "$env:_PACKAGE_VERSION"
VALUE "OriginalFilename", "bw.exe"
VALUE "InternalName", "bw"
VALUE "LegalCopyright", "Copyright Bitwarden Inc."
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0409 0x04B0
}
}
"@
$versionInfo | Out-File ./version-info.rc
# https://github.com/vercel/pkg-fetch/issues/188
- name: Resource Hacker
shell: cmd
run: |
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
set WIN_PKG=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\fetched-v%_WIN_PKG_FETCH_VERSION%-win-x64
set WIN_PKG_BUILT=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\built-v%_WIN_PKG_FETCH_VERSION%-win-x64
copy %WIN_PKG% %WIN_PKG_BUILT%
ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action delete -mask ICONGROUP,1,
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action addoverwrite -resource version-info.res
- name: Setup sub-module
run: npm run sub:init
- name: Install
run: npm ci
- name: Run tests
run: npm run test
- name: Build & Package
run: npm run dist
- name: Package Chocolatey
shell: pwsh
run: |
Copy-Item -Path stores/chocolatey -Destination dist/chocolatey -Recurse
Copy-Item dist/windows/bw.exe -Destination dist/chocolatey/tools
Copy-Item LICENSE.txt -Destination dist/chocolatey/tools
choco pack dist/chocolatey/bitwarden-cli.nuspec --version ${{ env._PACKAGE_VERSION }} --out dist/chocolatey
- name: Zip
shell: cmd
run: |
7z a ./dist/bw-windows-%_PACKAGE_VERSION%.zip ./dist/windows/bw.exe
7z a ./dist/bw-macos-%_PACKAGE_VERSION%.zip ./dist/macos/bw
7z a ./dist/bw-linux-%_PACKAGE_VERSION%.zip ./dist/linux/bw
- name: Version Test
run: |
dir ./dist/
Expand-Archive -Path "./dist/bw-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bw.exe -v'
echo "version: $env:_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if($testVersion -ne $env:_PACKAGE_VERSION) {
Throw "Version test failed."
}
- name: Create checksums
run: |
checksum -f="./dist/bw-windows-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File -Encoding ASCII ./dist/bw-windows-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist/bw-macos-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File -Encoding ASCII ./dist/bw-macos-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist/bw-linux-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File -Encoding ASCII ./dist/bw-linux-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload windows zip asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist/bw-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload windows checksum asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist/bw-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload macos zip asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist/bw-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload macos checksum asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist/bw-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload linux zip asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist/bw-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload linux checksum asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist/bw-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload Chocolatey asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
path: ./dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload NPM Build Directory asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
path: ./build
if-no-files-found: error
snap:
name: Build Snap
runs-on: ubuntu-20.04
needs: [setup, cli]
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Print environment
run: |
whoami
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
echo "BW Package Version: $_PACKAGE_VERSION"
- name: Get bw linux cli
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
with:
name: bw-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist/snap
- name: Setup Snap Package
run: |
cp -r stores/snap/* -t dist/snap
sed -i s/__version__/${{ env._PACKAGE_VERSION }}/g dist/snap/snapcraft.yaml
cd dist/snap
ls -alth
- name: Build snap
uses: snapcore/action-build@a400bf1c2d0f23074aaacf08a144813c3c20b35d # v1.0.9
with:
path: dist/snap
- name: Create checksum
run: |
cd dist/snap
ls -alth
sha256sum bw_${{ env._PACKAGE_VERSION }}_amd64.snap \
| awk '{split($0, a); print a[1]}' > bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
- name: Install Snap
run: sudo snap install dist/snap/bw*.snap --dangerous
- name: Test Snap
shell: pwsh
run: |
$testVersion = Invoke-Expression '& bw -v'
if($testVersion -ne $env:_PACKAGE_VERSION) {
Throw "Version test failed."
}
env:
BITWARDENCLI_APPDATA_DIR: "/home/runner/snap/bw/x1/.config/Bitwarden CLI"
- name: Cleanup Test & Update Snap for Publish
shell: pwsh
run: sudo snap remove bw
- name: Upload snap asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
path: ./dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload snap checksum asset
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- lint
- cli
- snap
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
LINT_STATUS: ${{ needs.lint.result }}
CLI_STATUS: ${{ needs.cli.result }}
SNAP_STATUS: ${{ needs.snap.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$SNAP_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

View File

@ -0,0 +1,16 @@
---
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

203
apps/cli/.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,203 @@
---
name: Release
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Options'
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
- Dry Run
jobs:
setup:
name: Setup
runs-on: ubuntu-20.04
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
branch-name: ${{ steps.branch.outputs.branch-name }}
steps:
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
- name: Retrieve CLI release version
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version package.json)
echo "::set-output name=package_version::$PKG_VERSION"
- name: Check to make sure CLI release version has been bumped
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
latest_ver=$(hub release -L 1 -f '%T')
latest_ver=${latest_ver:1}
echo "Latest version: $latest_ver"
ver=${{ steps.retrieve-version.outputs.package_version }}
echo "Version: $ver"
if [ "$latest_ver" = "$ver" ]; then
echo "Version has not been bumped!"
exit 1
fi
shell: bash
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
- name: Download all artifacts
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }}
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
env:
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
with:
artifacts: "bw-windows-${{ env.PKG_VERSION }}.zip,
bw-windows-sha256-${{ env.PKG_VERSION }}.txt,
bw-macos-${{ env.PKG_VERSION }}.zip,
bw-macos-sha256-${{ env.PKG_VERSION }}.txt,
bw-linux-${{ env.PKG_VERSION }}.zip,
bw-linux-sha256-${{ env.PKG_VERSION }}.txt,
bitwarden-cli.${{ env.PKG_VERSION }}.nupkg,
bw_${{ env.PKG_VERSION }}_amd64.snap,
bw-snap-sha256-${{ env.PKG_VERSION }}.txt"
commit: ${{ github.sha }}
tag: v${{ env.PKG_VERSION }}
name: Version ${{ env.PKG_VERSION }}
body: "<insert release notes here>"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
snap:
name: Deploy Snap
runs-on: ubuntu-20.04
needs: setup
env:
_PKG_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Login to Azure
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
with:
keyvault: "bitwarden-prod-kv"
secrets: "snapcraft-store-token"
- name: Install Snap
uses: samuelmeuli/action-snapcraft@10d7d0a84d9d86098b19f872257df314b0bd8e2d # v1.2.0
with:
snapcraft_token: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }}
- name: Download artifacts
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: bw_${{ env._PKG_VERSION }}_amd64.snap
- name: Publish Snap & logout
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
snapcraft push bw_${{ env._PKG_VERSION }}_amd64.snap --release stable
snapcraft logout
choco:
name: Deploy Choco
runs-on: windows-2019
needs: setup
env:
_PKG_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Setup Chocolatey
run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/
env:
CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }}
- name: Make dist dir
shell: pwsh
run: New-Item -ItemType directory -Path ./dist
- name: Download artifacts
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: bitwarden-cli.${{ env._PKG_VERSION }}.nupkg
path: ./dist
- name: Push to Chocolatey
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
shell: pwsh
run: |
cd dist
choco push
npm:
name: Publish NPM
runs-on: ubuntu-20.04
needs: setup
env:
_PKG_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download artifacts
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip
path: build
- name: Setup NPM
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install Husky
run: npm install -g husky
- name: Publish NPM
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: npm publish --access public

View File

@ -0,0 +1,71 @@
---
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
jobs:
bump_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Package
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package.json"
- name: Bump Version - Package-lock
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package-lock.json"
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"

View File

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

5
apps/cli/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
build
dist
config/local.json

1
apps/cli/.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

3
apps/cli/.npmignore Normal file
View File

@ -0,0 +1,3 @@
*
!/build
!/build/**/*

12
apps/cli/.prettierignore Normal file
View File

@ -0,0 +1,12 @@
# Build directories
build
dist
coverage
jslib
# External libraries / auto synced locales
src/locales
# Github Workflows
.github/workflows

View File

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

25
apps/cli/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"protocol": "inspector",
"cwd": "${workspaceRoot}",
"program": "${workspaceFolder}/build/bw.js",
"env": {
"BW_SESSION": "fPZb0J+1NBzQ+HB512pLhSIIt2aRoOjqs6SrbxbTHVcsZdFk1cthzjBIMqBa2X7fjOOA3VU0bnR42fYeuWj2Vw=="
},
"sourceMapPathOverrides": {
"meteor://💻app/*": "${workspaceFolder}/*",
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
"webpack://?:*/*": "${workspaceFolder}/*",
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
},
"smartStep": true,
"console": "integratedTerminal",
"args": ["login", "sdfsd@sdfdf.com", "ddddddd"]
}
]
}

10
apps/cli/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"debug.javascript.terminalOptions": {
"sourceMapPathOverrides": {
"meteor://💻app/*": "${workspaceFolder}/*",
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
"webpack://?:*/*": "${workspaceFolder}/*",
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
}
}
}

23
apps/cli/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,23 @@
# How to Contribute
Contributions of all kinds are welcome!
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
Here is how you can get involved:
- **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
- **Report a bug or submit a bugfix:** Use Github issues and pull requests
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
- **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
## Contributor Agreement
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/cli) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
## Pull Request Guidelines
- use `npm run lint` and fix any linting suggestions before submitting a pull request
- commit any pull requests against the `master` branch
- include a link to your Community Forums post

View File

@ -0,0 +1,52 @@
<!--
Please do not submit feature requests. The [Community Forums][1] has a
section for submitting, voting for, and discussing product feature requests.
[1]: https://community.bitwarden.com
-->
## Describe the Bug
<!-- Comment:
A clear and concise description of what the bug is.
-->
## Steps To Reproduce
<!-- Comment:
How can we reproduce the behavior:
-->
1. Launch using command, '...'
2. Copy value '....'
3. Pipe to '....'
4. Run the following command, '...'
## Expected Result
<!-- Comment:
A clear and concise description of what you expected to happen.
-->
## Actual Result
<!-- Comment:
A clear and concise description of what is happening.
-->
## Screenshots or Videos
<!-- Comment:
If applicable, add screenshots and/or a short video to help explain your problem.
-->
## Environment
- Operating system: [e.g. Windows 10, macOS BigSur, Ubuntu]
- Shell: [e.g. bash, zsh, PowerShell]
- Build Version (run `bw --version`): [e.g. 1.15.1]
## Additional Context
<!-- Comment:
Add any other context about the problem here.
-->

674
apps/cli/LICENSE.txt Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

120
apps/cli/README.md Normal file
View File

@ -0,0 +1,120 @@
> **Repository Reorganization in Progress**
>
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
>
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
[![Github Workflow build on master](https://github.com/bitwarden/cli/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/bitwarden/cli/actions/workflows/build.yml?query=branch:master)
[![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# Bitwarden Command-line Interface
[![Platforms](https://imgur.com/AnTLX0S.png "Platforms")](https://help.bitwarden.com/article/cli/#download--install)
The Bitwarden CLI is a powerful, full-featured command-line interface (CLI) tool to access and manage a Bitwarden vault. The CLI is written with TypeScript and Node.js and can be run on Windows, macOS, and Linux distributions.
![CLI](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/cli-macos.png "CLI")
## Download/Install
You can install the Bitwarden CLI multiple different ways:
**NPM**
If you already have the Node.js runtime installed on your system, you can install the CLI using NPM. NPM makes it easy to keep your installation updated and should be the preferred installation method if you are already using Node.js.
```bash
npm install -g @bitwarden/cli
```
**Native Executable**
We provide natively packaged versions of the CLI for each platform which have no requirements on installing the Node.js runtime. You can obtain these from the [downloads section](https://help.bitwarden.com/article/cli/#download--install) in the documentation.
**Other Package Managers**
- [Chocolatey](https://chocolatey.org/packages/bitwarden-cli)
```powershell
choco install bitwarden-cli
```
- [Homebrew](https://formulae.brew.sh/formula/bitwarden-cli)
```bash
brew install bitwarden-cli
```
- [Snap](https://snapcraft.io/bw)
```bash
sudo snap install bw
```
## Documentation
The Bitwarden CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
```bash
bw --help
```
This option will list all available commands that you can use with the CLI.
Additionally, you can run the `--help` option on a specific command to learn more about it:
```bash
bw list --help
bw create --help
```
**Detailed Documentation**
We provide detailed documentation and examples for using the CLI in our help center at https://help.bitwarden.com/article/cli/.
## Build/Run
**Requirements**
- [Node.js](https://nodejs.org) v16.13.1.
- Testing is done against Node 16, other versions may work, but are not guaranteed.
- NPM v8
**Run the app**
```bash
npm install
npm run sub:init # initialize the git submodule for jslib
npm run build:watch
```
You can then run commands from the `./build` folder:
```bash
node ./build/bw.js login
```
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
## Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge ec53a16c005e0dd9aef6845c18811e8b14067168`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 910b4a24e649f21acbf4da5b2d422b121d514bd5`
7. Push
### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

21
apps/cli/SECURITY.md Normal file
View File

@ -0,0 +1,21 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
While researching, we'd like to ask you to refrain from:
- Denial of service
- Spamming
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe!

31
apps/cli/config/config.js Normal file
View File

@ -0,0 +1,31 @@
/* eslint-disable no-console */
function load(envName) {
return {
...loadConfig(envName),
...loadConfig("local"),
};
}
function log(configObj) {
const repeatNum = 50;
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
console.log(JSON.stringify(configObj, null, 2));
console.log(`${"=".repeat(repeatNum)}`);
}
function loadConfig(configName) {
try {
return require(`./${configName}.json`);
} catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
return {};
} else {
throw e;
}
}
}
module.exports = {
load,
log,
};

View File

@ -0,0 +1,5 @@
{
"flags": {
"serve": true
}
}

View File

@ -0,0 +1,5 @@
{
"flags": {
"serve": true
}
}

View File

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# bw git-credential helper
# Based on:
# * https://github.com/lastpass/lastpass-cli/blob/master/contrib/examples/git-credential-lastpass
# * https://gist.github.com/mikeboiko/58ab730afd65bca0a125bc12b6f4670d
# A credential helper for git to retrieve usernames and passwords from bw.
# For general usage, see https://git-scm.com/docs/gitcredentials.
# Here's a quick version:
# 1. Put this somewhere in your path.
# 2. git config --global credential.helper bw
declare -A params
if [[ "$1" == "get" ]]; then
read -r line
while [ -n "$line" ]; do
key=${line%%=*}
value=${line#*=}
params[$key]=$value
read -r line
done
if [[ "${params['protocol']}" != "https" ]]; then
exit
fi
if [[ -z "${params["host"]}" ]]; then
exit
fi
if ! bw list items --search "asdf" > /dev/null 2>&1; then
echo "Please login to Bitwarden to use git credential helper" > /dev/stderr
exit
fi
id=$(bw list items --search "${params["host"]}"|jq ".[] | select(.name == \"${params["host"]}\").id" -r)
if [[ -z "$id" ]]; then
echo "Couldn't find item id in Bitwarden DB." > /dev/stderr
echo "${params}"
exit
fi
user=$(bw get username "${id}")
pass=$(bw get password "${id}")
if [[ -z "$user" ]] || [[ -z "$pass" ]]; then
echo "Couldn't find host in Bitwarden DB." > /dev/stderr
exit
fi
echo username="$user"
echo password="$pass"
fi

1
apps/cli/jslib Submodule

@ -0,0 +1 @@
Subproject commit 1370006f6ea310cf85a12bcbd8213f74f9552c4d

12969
apps/cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

137
apps/cli/package.json Normal file
View File

@ -0,0 +1,137 @@
{
"name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.",
"version": "1.22.1",
"keywords": [
"bitwarden",
"password",
"vault",
"password manager",
"cli"
],
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/cli"
},
"license": "GPL-3.0-only",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull origin master",
"clean": "rimraf dist/**/*",
"symlink:win": "rmdir /S /Q ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build": "webpack",
"build:debug": "npm run build && node --inspect ./build/bw.js",
"build:watch": "webpack --watch",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"package": "npm run package:win && npm run package:mac && npm run package:lin",
"package:win": "pkg . --targets win-x64 --output ./dist/windows/bw.exe --build",
"package:mac": "pkg . --targets macos-x64 --output ./dist/macos/bw",
"package:lin": "pkg . --targets linux-x64 --output ./dist/linux/bw",
"debug": "node --inspect ./build/bw.js",
"dist": "npm run build:prod && npm run clean && npm run package",
"dist:win": "npm run build:prod && npm run clean && npm run package:win",
"dist:mac": "npm run build:prod && npm run clean && npm run package:mac",
"dist:lin": "npm run build:prod && npm run clean && npm run package:lin",
"publish:npm": "npm run build:prod && npm publish --access public",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"prettier": "prettier --write .",
"prepare": "husky install",
"test": "jasmine-ts -r tsconfig-paths/register -P spec/tsconfig.json",
"test:watch": "nodemon -w ./spec -w ./src -w ./jslib --ext \"ts,js,mjs,json\" --exec jasmine-ts -r tsconfig-paths/register -P spec/tsconfig.json"
},
"bin": {
"bw": "build/bw.js"
},
"pkg": {
"assets": "./build/**/*"
},
"devDependencies": {
"@fluffy-spoon/substitute": "^1.208.0",
"@types/inquirer": "^7.3.1",
"@types/jasmine": "^3.7.0",
"@types/jsdom": "^16.2.10",
"@types/koa": "^2.13.4",
"@types/koa__multer": "^2.0.4",
"@types/koa__router": "^8.0.11",
"@types/koa-bodyparser": "^4.3.5",
"@types/koa-json": "^2.0.20",
"@types/lowdb": "^1.0.10",
"@types/lunr": "^2.3.3",
"@types/node": "^16.11.12",
"@types/node-fetch": "^2.5.10",
"@types/node-forge": "^1.0.1",
"@types/papaparse": "^5.2.5",
"@types/proper-lockfile": "^4.1.2",
"@types/retry": "^0.12.1",
"@types/tldjs": "^2.3.0",
"@types/zxcvbn": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.4.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"husky": "^7.0.4",
"jasmine": "^3.7.0",
"jasmine-core": "^3.7.1",
"jasmine-ts": "^0.4.0",
"jasmine-ts-console-reporter": "^3.1.1",
"lint-staged": "^12.1.3",
"pkg": "^5.5.1",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"ts-loader": "^8.2.0",
"ts-node": "^10.4.0",
"tsconfig-paths": "^3.12.0",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "4.1.5",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
"@koa/multer": "^3.0.0",
"@koa/router": "^10.1.1",
"big-integer": "1.6.48",
"browser-hrtime": "^1.1.8",
"chalk": "^4.1.1",
"commander": "7.2.0",
"form-data": "4.0.0",
"https-proxy-agent": "5.0.0",
"inquirer": "8.0.0",
"jsdom": "^16.5.3",
"jszip": "^3.7.1",
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
"koa-json": "^2.0.2",
"lowdb": "1.0.0",
"lunr": "^2.3.9",
"multer": "^1.4.4",
"node-fetch": "^2.6.1",
"node-forge": "1.3.1",
"open": "^8.0.8",
"papaparse": "^5.3.0",
"proper-lockfile": "^4.1.2",
"rxjs": "6.6.7",
"tldjs": "^2.3.1",
"zxcvbn": "^4.4.2"
},
"engines": {
"node": "~16",
"npm": ">=7 <=8"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix"
}
}

View File

@ -0,0 +1,23 @@
param (
[Parameter(Mandatory=$true)]
[string] $version
)
# Dependencies:
# 1. brew cask install powershell
# 2. Environment variables for HOMEBREW_GITHUB_USER and HOMEBREW_GITHUB_API_TOKEN set.
#
# To run:
# pwsh ./brew-update.ps1 -version 1.1.0
# Cleaning up
cd $("$(brew --repository)" + "/Library/Taps/homebrew/homebrew-core/Formula")
git checkout master
git reset --hard origin/master
git push $env:HOMEBREW_GITHUB_USER master
git branch -D $("bitwarden-cli-" + $version)
git push $env:HOMEBREW_GITHUB_USER --delete $("bitwarden-cli-" + $version)
# Bump
$url = 'https://registry.npmjs.org/@bitwarden/cli/-/cli-' + $version + '.tgz';
brew bump-formula-pr --url="$url" bitwarden-cli

View File

@ -0,0 +1,34 @@
param (
[switch] $push
)
# To run:
# .\choco-pack.ps1
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path;
$rootDir = $dir + "\..";
$distDir = $rootDir + "\dist";
$chocoDir = $rootDir + "\stores\chocolatey";
$distChocoDir = $distDir + "\chocolatey";
$distChocoToolsDir = $distDir + "\chocolatey\tools";
if(Test-Path -Path $distChocoDir) {
Remove-Item -Recurse -Force $distChocoDir
}
$exe = $distDir + "\windows\bw.exe";
$license = $rootDir + "\LICENSE.txt";
Copy-Item -Path $chocoDir -Destination $distChocoDir Recurse
Copy-Item $exe -Destination $distChocoToolsDir;
Copy-Item $license -Destination $distChocoToolsDir;
$srcPackage = $rootDir + "\package.json";
$srcPackageVersion = (Get-Content -Raw -Path $srcPackage | ConvertFrom-Json).version;
$nuspec = $distChocoDir + "\bitwarden-cli.nuspec";
choco pack $nuspec --version $srcPackageVersion --out $distChocoDir
if ($push) {
cd $distChocoDir
choco push
cd $rootDir
}

View File

@ -0,0 +1,26 @@
param (
[Parameter(Mandatory=$true)]
[string] $version
)
# To run:
# .\choco-update.ps1 -version 1.3.0
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path;
$rootDir = $dir + "\..";
$distDir = $rootDir + "\dist";
$distChocoDir = $distDir + "\chocolatey";
if(Test-Path -Path $distChocoDir) {
Remove-Item -Recurse -Force $distChocoDir
}
New-Item -ItemType directory -Path $distChocoDir | Out-Null
$nupkg = "bitwarden-cli." + $version + ".nupkg"
$uri = "https://github.com/bitwarden/cli/releases/download/v" + $version + "/" + $nupkg;
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-RestMethod -Uri $uri -OutFile $($distChocoDir + "\" + $nupkg)
cd $distChocoDir
choco push
cd $rootDir

View File

@ -0,0 +1,32 @@
param (
[Parameter(Mandatory=$true)]
[string] $version
)
# Dependencies:
# 1. Install powershell, ex `sudo apt-get install -y powershell`
#
# To run:
# ./snap-build.ps1 -version 1.1.0
#
# and then push to snap with:
# cd ../dist/snap
# snap push bw*.snap
# or, use the ./snap-update.ps1 script
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = $dir + "/.."
$distDir = $rootDir + "/dist"
$snapDir = $rootDir + "/stores/snap"
$distSnapDir = $distDir + "/snap"
$snapDistYaml = $distSnapDir + "/snapcraft.yaml"
if(Test-Path -Path $distSnapDir) {
Remove-Item -Recurse -Force $distSnapDir
}
Copy-Item -Path $snapDir -Destination $distSnapDir Recurse
(Get-Content $snapDistYaml).replace('__version__', $version) | Set-Content $snapDistYaml
cd $distSnapDir
snapcraft
cd $rootDir

View File

@ -0,0 +1,12 @@
# Dependencies:
# 1. Install powershell, ex `sudo apt-get install -y powershell`
#
# To run:
# pwsh ./snap-update.ps1
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path;
$rootDir = $dir + "/..";
$distDir = $rootDir + "/dist";
$distSnap = $distDir + "/snap/bw*.snap";
snapcraft push $distSnap --release stable

3
apps/cli/spec/bw.spec.ts Normal file
View File

@ -0,0 +1,3 @@
describe("bw", () => {
it("is a placeholder test");
});

4
apps/cli/spec/helpers.ts Normal file
View File

@ -0,0 +1,4 @@
// eslint-disable-next-line
const TSConsoleReporter = require("jasmine-ts-console-reporter");
jasmine.getEnv().clearReporters(); // Clear default console reporter
jasmine.getEnv().addReporter(new TSConsoleReporter());

View File

@ -0,0 +1,7 @@
{
"spec_dir": "spec",
"spec_files": ["**/*[sS]pec.ts"],
"helpers": ["helpers.ts"],
"stopSpecOnExpectationFailure": false,
"random": true
}

View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig",
"include": ["src", "spec"],
"compilerOptions": {
"module": "commonjs"
}
}

View File

@ -0,0 +1,27 @@
import { FlagName } from "../src/flags";
import { CliUtils } from "../src/utils";
describe("flagEnabled", () => {
it("is true if flag is null", () => {
process.env.FLAGS = JSON.stringify({ test: null });
expect(CliUtils.flagEnabled("test" as FlagName)).toBe(true);
});
it("is true if flag is undefined", () => {
process.env.FLAGS = JSON.stringify({});
expect(CliUtils.flagEnabled("test" as FlagName)).toBe(true);
});
it("is true if flag is true", () => {
process.env.FLAGS = JSON.stringify({ test: true });
expect(CliUtils.flagEnabled("test" as FlagName)).toBe(true);
});
it("is false if flag is false", () => {
process.env.FLAGS = JSON.stringify({ test: false });
expect(CliUtils.flagEnabled("test" as FlagName)).toBe(false);
});
});

380
apps/cli/src/bw.ts Normal file
View File

@ -0,0 +1,380 @@
import * as fs from "fs";
import * as path from "path";
import * as program from "commander";
import * as jsdom from "jsdom";
import { ClientType } from "jslib-common/enums/clientType";
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
import { LogLevelType } from "jslib-common/enums/logLevelType";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { Account } from "jslib-common/models/domain/account";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { AppIdService } from "jslib-common/services/appId.service";
import { AuditService } from "jslib-common/services/audit.service";
import { AuthService } from "jslib-common/services/auth.service";
import { CipherService } from "jslib-common/services/cipher.service";
import { CollectionService } from "jslib-common/services/collection.service";
import { ContainerService } from "jslib-common/services/container.service";
import { CryptoService } from "jslib-common/services/crypto.service";
import { EnvironmentService } from "jslib-common/services/environment.service";
import { ExportService } from "jslib-common/services/export.service";
import { FileUploadService } from "jslib-common/services/fileUpload.service";
import { FolderService } from "jslib-common/services/folder.service";
import { ImportService } from "jslib-common/services/import.service";
import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
import { NoopMessagingService } from "jslib-common/services/noopMessaging.service";
import { OrganizationService } from "jslib-common/services/organization.service";
import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
import { PolicyService } from "jslib-common/services/policy.service";
import { ProviderService } from "jslib-common/services/provider.service";
import { SearchService } from "jslib-common/services/search.service";
import { SendService } from "jslib-common/services/send.service";
import { SettingsService } from "jslib-common/services/settings.service";
import { StateService } from "jslib-common/services/state.service";
import { StateMigrationService } from "jslib-common/services/stateMigration.service";
import { SyncService } from "jslib-common/services/sync.service";
import { TokenService } from "jslib-common/services/token.service";
import { TotpService } from "jslib-common/services/totp.service";
import { TwoFactorService } from "jslib-common/services/twoFactor.service";
import { UserVerificationService } from "jslib-common/services/userVerification.service";
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
import { NodeApiService } from "jslib-node/services/nodeApi.service";
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
import { Program } from "./program";
import { SendProgram } from "./send.program";
import { I18nService } from "./services/i18n.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NodeEnvSecureStorageService } from "./services/nodeEnvSecureStorage.service";
import { VaultProgram } from "./vault.program";
// Polyfills
(global as any).DOMParser = new jsdom.JSDOM().window.DOMParser;
// eslint-disable-next-line
const packageJson = require("../package.json");
export class Main {
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
cryptoService: CryptoService;
tokenService: TokenService;
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
settingsService: SettingsService;
cipherService: CipherService;
folderService: FolderService;
collectionService: CollectionService;
vaultTimeoutService: VaultTimeoutService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
totpService: TotpService;
containerService: ContainerService;
auditService: AuditService;
importService: ImportService;
exportService: ExportService;
searchService: SearchService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
policyService: PolicyService;
program: Program;
vaultProgram: VaultProgram;
sendProgram: SendProgram;
logService: ConsoleLogService;
sendService: SendService;
fileUploadService: FileUploadService;
keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService;
stateService: StateService;
stateMigrationService: StateMigrationService;
organizationService: OrganizationService;
providerService: ProviderService;
twoFactorService: TwoFactorService;
constructor() {
let p = null;
const relativeDataDir = path.join(path.dirname(process.execPath), "bw-data");
if (fs.existsSync(relativeDataDir)) {
p = relativeDataDir;
} else if (process.env.BITWARDENCLI_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR);
} else if (process.platform === "darwin") {
p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI");
} else if (process.platform === "win32") {
p = path.join(process.env.APPDATA, "Bitwarden CLI");
} else if (process.env.XDG_CONFIG_HOME) {
p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI");
} else {
p = path.join(process.env.HOME, ".config/Bitwarden CLI");
}
this.i18nService = new I18nService("en", "./locales");
this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson);
this.logService = new ConsoleLogService(
this.platformUtilsService.isDev(),
(level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info
);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
this.secureStorageService = new NodeEnvSecureStorageService(
this.storageService,
this.logService,
() => this.cryptoService
);
this.stateMigrationService = new StateMigrationService(
this.storageService,
this.secureStorageService,
new StateFactory(GlobalState, Account)
);
this.stateService = new StateService(
this.storageService,
this.secureStorageService,
this.logService,
this.stateMigrationService,
new StateFactory(GlobalState, Account)
);
this.cryptoService = new CryptoService(
this.cryptoFunctionService,
this.platformUtilsService,
this.logService,
this.stateService
);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.stateService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.stateService);
const customUserAgent =
"Bitwarden_CLI/" +
this.platformUtilsService.getApplicationVersionSync() +
" (" +
this.platformUtilsService.getDeviceString().toUpperCase() +
")";
this.apiService = new NodeApiService(
this.tokenService,
this.platformUtilsService,
this.environmentService,
this.appIdService,
async (expired: boolean) => await this.logout(),
customUserAgent
);
this.containerService = new ContainerService(this.cryptoService);
this.settingsService = new SettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.fileUploadService,
this.i18nService,
null,
this.logService,
this.stateService
);
this.folderService = new FolderService(
this.cryptoService,
this.apiService,
this.i18nService,
this.cipherService,
this.stateService
);
this.collectionService = new CollectionService(
this.cryptoService,
this.i18nService,
this.stateService
);
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
this.providerService = new ProviderService(this.stateService);
this.organizationService = new OrganizationService(this.stateService);
this.policyService = new PolicyService(
this.stateService,
this.organizationService,
this.apiService
);
this.sendService = new SendService(
this.cryptoService,
this.apiService,
this.fileUploadService,
this.i18nService,
this.cryptoFunctionService,
this.stateService
);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
this.cryptoService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.cryptoFunctionService
);
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService
);
const lockedCallback = async () =>
await this.cryptoService.clearStoredKey(KeySuffixOptions.Auto);
this.vaultTimeoutService = new VaultTimeoutService(
this.cipherService,
this.folderService,
this.collectionService,
this.cryptoService,
this.platformUtilsService,
this.messagingService,
this.searchService,
this.tokenService,
this.policyService,
this.keyConnectorService,
this.stateService,
this.authService,
lockedCallback,
null
);
this.syncService = new SyncService(
this.apiService,
this.settingsService,
this.folderService,
this.cipherService,
this.cryptoService,
this.collectionService,
this.messagingService,
this.policyService,
this.sendService,
this.logService,
this.keyConnectorService,
this.stateService,
this.organizationService,
this.providerService,
async (expired: boolean) => await this.logout()
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.totpService = new TotpService(
this.cryptoFunctionService,
this.logService,
this.stateService
);
this.importService = new ImportService(
this.cipherService,
this.folderService,
this.apiService,
this.i18nService,
this.collectionService,
this.platformUtilsService,
this.cryptoService
);
this.exportService = new ExportService(
this.folderService,
this.cipherService,
this.apiService,
this.cryptoService,
this.cryptoFunctionService
);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
this.program = new Program(this);
this.vaultProgram = new VaultProgram(this);
this.sendProgram = new SendProgram(this);
this.userVerificationService = new UserVerificationService(
this.cryptoService,
this.i18nService,
this.apiService
);
}
async run() {
await this.init();
await this.program.register();
await this.vaultProgram.register();
await this.sendProgram.register();
program.parse(process.argv);
if (process.argv.slice(2).length === 0) {
program.outputHelp();
}
}
async logout() {
this.authService.logOut(() => {
/* Do nothing */
});
const userId = await this.stateService.getUserId();
await Promise.all([
this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
]);
await this.stateService.clean();
process.env.BW_SESSION = null;
}
private async init() {
await this.storageService.init();
await this.stateService.init();
this.containerService.attachToWindow(global);
await this.environmentService.setUrlsFromStorage();
const locale = await this.stateService.getLocale();
await this.i18nService.init(locale);
this.twoFactorService.init();
const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.stateService.setInstalledVersion(currentVersion);
}
}
}
const main = new Main();
main.run();

View File

@ -0,0 +1,119 @@
import * as program from "commander";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
interface IOption {
long?: string;
short?: string;
description: string;
}
interface ICommand {
commands?: ICommand[];
options?: IOption[];
_name: string;
_description: string;
}
const validShells = ["zsh"];
export class CompletionCommand {
async run(options: program.OptionValues) {
const shell: typeof validShells[number] = options.shell;
if (!shell) {
return Response.badRequest("`shell` option was not provided.");
}
if (!validShells.includes(shell)) {
return Response.badRequest("Unsupported shell.");
}
let content = "";
if (shell === "zsh") {
content = this.zshCompletion("bw", program as any as ICommand).render();
}
const res = new MessageResponse(content, null);
return Response.success(res);
}
private zshCompletion(rootName: string, rootCommand: ICommand) {
return {
render: () => {
return [
`#compdef _${rootName} ${rootName}`,
"",
this.renderCommandBlock(rootName, rootCommand),
].join("\n");
},
};
}
private renderCommandBlock(name: string, command: ICommand): string {
const { commands = [], options = [] } = command;
const hasOptions = options.length > 0;
const hasCommands = commands.length > 0;
const args = options
.map(({ long, short, description }) => {
const aliases = [short, long].filter(Boolean);
const opts = aliases.join(",");
const desc = `[${description.replace(`'`, `'"'"'`)}]`;
return aliases.length > 1
? `'(${aliases.join(" ")})'{${opts}}'${desc}'`
: `'${opts}${desc}'`;
})
.concat(
`'(-h --help)'{-h,--help}'[output usage information]'`,
hasCommands ? '"1: :->cmnds"' : null,
'"*::arg:->args"'
)
.filter(Boolean);
const commandBlockFunctionParts = [];
if (hasCommands) {
commandBlockFunctionParts.push("local -a commands");
}
if (hasOptions) {
commandBlockFunctionParts.push(`_arguments -C \\\n ${args.join(` \\\n `)}`);
}
if (hasCommands) {
commandBlockFunctionParts.push(
`case $state in
cmnds)
commands=(
${commands
.map(({ _name, _description }) => `"${_name}:${_description}"`)
.join("\n ")}
)
_describe "command" commands
;;
esac
case "$words[1]" in
${commands
.map(({ _name }) => [`${_name})`, `_${name}_${_name}`, ";;"].join("\n "))
.join("\n ")}
esac`
);
}
const commandBlocParts = [
`function _${name} {\n ${commandBlockFunctionParts.join("\n\n ")}\n}`,
];
if (hasCommands) {
commandBlocParts.push(
commands.map((c) => this.renderCommandBlock(`${name}_${c._name}`, c)).join("\n\n")
);
}
return commandBlocParts.join("\n\n");
}
}

View File

@ -0,0 +1,53 @@
import * as program from "commander";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
export class ConfigCommand {
constructor(private environmentService: EnvironmentService) {}
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase();
switch (setting) {
case "server":
return await this.getOrSetServer(value, options);
default:
return Response.badRequest("Unknown setting.");
}
}
private async getOrSetServer(url: string, options: program.OptionValues): Promise<Response> {
if (
(url == null || url.trim() === "") &&
!options.webVault &&
!options.api &&
!options.identity &&
!options.icons &&
!options.notifications &&
!options.events
) {
const stringRes = new StringResponse(
this.environmentService.hasBaseUrl()
? this.environmentService.getUrls().base
: "https://bitwarden.com"
);
return Response.success(stringRes);
}
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
await this.environmentService.setUrls({
base: url,
webVault: options.webVault || null,
api: options.api || null,
identity: options.identity || null,
icons: options.icons || null,
notifications: options.notifications || null,
events: options.events || null,
keyConnector: options.keyConnector || null,
});
const res = new MessageResponse("Saved setting `config`.", null);
return Response.success(res);
}
}

View File

@ -0,0 +1,62 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { Utils } from "jslib-common/misc/utils";
import { OrganizationUserConfirmRequest } from "jslib-common/models/request/organizationUserConfirmRequest";
import { Response } from "jslib-node/cli/models/response";
export class ConfirmCommand {
constructor(private apiService: ApiService, private cryptoService: CryptoService) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "org-member":
return await this.confirmOrganizationMember(id, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async confirmOrganizationMember(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const orgUser = await this.apiService.getOrganizationUser(options.organizationId, id);
if (orgUser == null) {
throw new Error("Member id does not exist for this organization.");
}
const publicKeyResponse = await this.apiService.getUserPublicKey(orgUser.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const req = new OrganizationUserConfirmRequest();
req.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(options.organizationId, id, req);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}
class Options {
organizationId: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
}
}

View File

@ -0,0 +1,84 @@
import * as inquirer from "inquirer";
import { ApiService } from "jslib-common/abstractions/api.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class ConvertToKeyConnectorCommand {
constructor(
private apiService: ApiService,
private keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
private logout: () => Promise<void>
) {}
async run(): Promise<Response> {
// If no interaction available, alert user to use web vault
const canInteract = process.env.BW_NOINTERACTION !== "true";
if (!canInteract) {
await this.logout();
return Response.error(
new MessageResponse(
"An organization you are a member of is using Key Connector. " +
"In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.",
null
)
);
}
const organization = await this.keyConnectorService.getManagingOrganization();
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "list",
name: "convert",
message:
organization.name +
" is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ",
choices: [
{
name: "Remove master password and unlock",
value: "remove",
},
{
name: "Leave organization and unlock",
value: "leave",
},
{
name: "Log out",
value: "exit",
},
],
});
if (answer.convert === "remove") {
try {
await this.keyConnectorService.migrateUser();
} catch (e) {
await this.logout();
throw e;
}
await this.keyConnectorService.removeConvertAccountRequired();
await this.keyConnectorService.setUsesKeyConnector(true);
// Update environment URL - required for api key login
const urls = this.environmentService.getUrls();
urls.keyConnector = organization.keyConnectorUrl;
await this.environmentService.setUrls(urls);
return Response.success();
} else if (answer.convert === "leave") {
await this.apiService.postLeaveOrganization(organization.id);
await this.keyConnectorService.removeConvertAccountRequired();
await this.syncService.fullSync(true);
return Response.success();
} else {
await this.logout();
return Response.error("You have been logged out.");
}
}
}

View File

@ -0,0 +1,206 @@
import * as fs from "fs";
import * as path from "path";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from "jslib-common/misc/utils";
import { CipherExport } from "jslib-common/models/export/cipherExport";
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { FolderExport } from "jslib-common/models/export/folderExport";
import { CollectionRequest } from "jslib-common/models/request/collectionRequest";
import { SelectionReadOnlyRequest } from "jslib-common/models/request/selectionReadOnlyRequest";
import { Response } from "jslib-node/cli/models/response";
import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CipherResponse } from "../models/response/cipherResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { CliUtils } from "../utils";
export class CreateCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService
) {}
async run(
object: string,
requestJson: string,
cmdOptions: Record<string, any>,
additionalData: any = null
): Promise<Response> {
let req: any = null;
if (object !== "attachment") {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
if (typeof requestJson !== "string") {
req = requestJson;
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.createCipher(req);
case "attachment":
return await this.createAttachment(normalizedOptions, additionalData);
case "folder":
return await this.createFolder(req);
case "org-collection":
return await this.createOrganizationCollection(req, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async createCipher(req: CipherExport) {
const cipher = await this.cipherService.encrypt(CipherExport.toView(req));
try {
await this.cipherService.saveWithServer(cipher);
const newCipher = await this.cipherService.get(cipher.id);
const decCipher = await newCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async createAttachment(options: Options, additionalData: any) {
if (options.itemId == null || options.itemId === "") {
return Response.badRequest("`itemid` option is required.");
}
let fileBuf: Buffer = null;
let fileName: string = null;
if (process.env.BW_SERVE === "true") {
fileBuf = additionalData.fileBuffer;
fileName = additionalData.fileName;
} else {
if (options.file == null || options.file === "") {
return Response.badRequest("`file` option is required.");
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(options.file)) {
return Response.badRequest("Cannot find file at " + filePath);
}
fileBuf = fs.readFileSync(filePath);
fileName = path.basename(filePath);
}
if (fileBuf == null) {
return Response.badRequest("File not provided.");
}
if (fileName == null || fileName.trim() === "") {
return Response.badRequest("File name not provided.");
}
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
const encKey = await this.cryptoService.getEncKey();
if (encKey == null) {
return Response.error(
"You must update your encryption key before you can use this feature. " +
"See https://help.bitwarden.com/article/update-encryption-key/"
);
}
try {
await this.cipherService.saveAttachmentRawWithServer(
cipher,
fileName,
new Uint8Array(fileBuf).buffer
);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
return Response.success(new CipherResponse(decCipher));
} catch (e) {
return Response.error(e);
}
}
private async createFolder(req: FolderExport) {
const folder = await this.folderService.encrypt(FolderExport.toView(req));
try {
await this.folderService.saveWithServer(folder);
const newFolder = await this.folderService.get(folder.id);
const decFolder = await newFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async createOrganizationCollection(req: OrganizationCollectionRequest, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
if (options.organizationId !== req.organizationId) {
return Response.badRequest("`organizationid` option does not match request object.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const groups =
req.groups == null
? null
: req.groups.map((g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords));
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
request.externalId = req.externalId;
request.groups = groups;
const response = await this.apiService.postCollection(req.organizationId, request);
const view = CollectionExport.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}
class Options {
itemId: string;
organizationId: string;
file: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
this.file = passedOptions?.file;
}
}

View File

@ -0,0 +1,131 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from "jslib-common/misc/utils";
import { Response } from "jslib-node/cli/models/response";
import { CliUtils } from "src/utils";
export class DeleteCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private stateService: StateService,
private apiService: ApiService
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.deleteCipher(id, normalizedOptions);
case "attachment":
return await this.deleteAttachment(id, normalizedOptions);
case "folder":
return await this.deleteFolder(id);
case "org-collection":
return await this.deleteOrganizationCollection(id, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async deleteCipher(id: string, options: Options) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
try {
if (options.permanent) {
await this.cipherService.deleteWithServer(id);
} else {
await this.cipherService.softDeleteWithServer(id);
}
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private async deleteAttachment(id: string, options: Options) {
if (options.itemId == null || options.itemId === "") {
return Response.badRequest("`itemid` option is required.");
}
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
}
if (cipher.attachments == null || cipher.attachments.length === 0) {
return Response.error("No attachments available for this item.");
}
const attachments = cipher.attachments.filter((a) => a.id.toLowerCase() === id);
if (attachments.length === 0) {
return Response.error("Attachment `" + id + "` was not found.");
}
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
try {
await this.cipherService.deleteAttachmentWithServer(cipher.id, attachments[0].id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private async deleteFolder(id: string) {
const folder = await this.folderService.get(id);
if (folder == null) {
return Response.notFound();
}
try {
await this.folderService.deleteWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private async deleteOrganizationCollection(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` options is required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
try {
await this.apiService.deleteCollection(options.organizationId, id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}
class Options {
itemId: string;
organizationId: string;
permanent: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
this.permanent = CliUtils.convertBooleanOption(passedOptions?.permanent);
}
}

View File

@ -0,0 +1,43 @@
import * as fet from "node-fetch";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { Response } from "jslib-node/cli/models/response";
import { FileResponse } from "jslib-node/cli/models/response/fileResponse";
import { CliUtils } from "../utils";
export abstract class DownloadCommand {
constructor(protected cryptoService: CryptoService) {}
protected async saveAttachmentToFile(
url: string,
key: SymmetricCryptoKey,
fileName: string,
output?: string
) {
const response = await fet.default(new fet.Request(url, { headers: { cache: "no-cache" } }));
if (response.status !== 200) {
return Response.error(
"A " + response.status + " error occurred while downloading the attachment."
);
}
try {
const buf = await response.arrayBuffer();
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
if (process.env.BW_SERVE === "true") {
const res = new FileResponse(Buffer.from(decBuf), fileName);
return Response.success(res);
} else {
return await CliUtils.saveResultToFile(Buffer.from(decBuf), output, fileName);
}
} catch (e) {
if (typeof e === "string") {
return Response.error(e);
} else {
return Response.error("An error occurred while saving the attachment.");
}
}
}
}

View File

@ -0,0 +1,186 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { Utils } from "jslib-common/misc/utils";
import { CipherExport } from "jslib-common/models/export/cipherExport";
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { FolderExport } from "jslib-common/models/export/folderExport";
import { CollectionRequest } from "jslib-common/models/request/collectionRequest";
import { SelectionReadOnlyRequest } from "jslib-common/models/request/selectionReadOnlyRequest";
import { Response } from "jslib-node/cli/models/response";
import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CipherResponse } from "../models/response/cipherResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { CliUtils } from "../utils";
export class EditCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private cryptoService: CryptoService,
private apiService: ApiService
) {}
async run(
object: string,
id: string,
requestJson: any,
cmdOptions: Record<string, any>
): Promise<Response> {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
let req: any = null;
if (typeof requestJson !== "string") {
req = requestJson;
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.editCipher(id, req);
case "item-collections":
return await this.editCipherCollections(id, req);
case "folder":
return await this.editFolder(id, req);
case "org-collection":
return await this.editOrganizationCollection(id, req, normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async editCipher(id: string, req: CipherExport) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
let cipherView = await cipher.decrypt();
if (cipherView.isDeleted) {
return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
}
cipherView = CipherExport.toView(req, cipherView);
const encCipher = await this.cipherService.encrypt(cipherView);
try {
await this.cipherService.saveWithServer(encCipher);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async editCipherCollections(id: string, req: string[]) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId == null) {
return Response.badRequest(
"Item does not belong to an organization. Consider moving it first."
);
}
cipher.collectionIds = req;
try {
await this.cipherService.saveCollectionsWithServer(cipher);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async editFolder(id: string, req: FolderExport) {
const folder = await this.folderService.get(id);
if (folder == null) {
return Response.notFound();
}
let folderView = await folder.decrypt();
folderView = FolderExport.toView(req, folderView);
const encFolder = await this.folderService.encrypt(folderView);
try {
await this.folderService.saveWithServer(encFolder);
const updatedFolder = await this.folderService.get(folder.id);
const decFolder = await updatedFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async editOrganizationCollection(
id: string,
req: OrganizationCollectionRequest,
options: Options
) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
if (options.organizationId !== req.organizationId) {
return Response.badRequest("`organizationid` option does not match request object.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const groups =
req.groups == null
? null
: req.groups.map((g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords));
const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
request.externalId = req.externalId;
request.groups = groups;
const response = await this.apiService.putCollection(req.organizationId, id, request);
const view = CollectionExport.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}
class Options {
organizationId: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
}
}

View File

@ -0,0 +1,16 @@
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "../utils";
export class EncodeCommand {
async run(): Promise<Response> {
if (process.stdin.isTTY) {
return Response.badRequest("No stdin was piped in.");
}
const input = await CliUtils.readStdin();
const b64 = Buffer.from(input, "utf8").toString("base64");
const res = new StringResponse(b64);
return Response.success(res);
}
}

View File

@ -0,0 +1,97 @@
import * as program from "commander";
import * as inquirer from "inquirer";
import { ExportFormat, ExportService } from "jslib-common/abstractions/export.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { PolicyType } from "jslib-common/enums/policyType";
import { Utils } from "jslib-common/misc/utils";
import { Response } from "jslib-node/cli/models/response";
import { CliUtils } from "../utils";
export class ExportCommand {
constructor(private exportService: ExportService, private policyService: PolicyService) {}
async run(options: program.OptionValues): Promise<Response> {
if (
options.organizationid == null &&
(await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport))
) {
return Response.badRequest(
"One or more organization policies prevents you from exporting your personal vault."
);
}
const format = options.format ?? "csv";
if (options.organizationid != null && !Utils.isGuid(options.organizationid)) {
return Response.error("`" + options.organizationid + "` is not a GUID.");
}
let exportContent: string = null;
try {
exportContent =
format === "encrypted_json"
? await this.getProtectedExport(options.password, options.organizationid)
: await this.getUnprotectedExport(format, options.organizationid);
} catch (e) {
return Response.error(e);
}
return await this.saveFile(exportContent, options, format);
}
private async getProtectedExport(passwordOption: string | boolean, organizationId?: string) {
const password = await this.promptPassword(passwordOption);
return password == null
? await this.exportService.getExport("encrypted_json", organizationId)
: await this.exportService.getPasswordProtectedExport(password, organizationId);
}
private async getUnprotectedExport(format: ExportFormat, organizationId?: string) {
return this.exportService.getExport(format, organizationId);
}
private async saveFile(
exportContent: string,
options: program.OptionValues,
format: ExportFormat
): Promise<Response> {
try {
const fileName = this.getFileName(format, options.organizationid != null ? "org" : null);
return await CliUtils.saveResultToFile(exportContent, options.output, fileName);
} catch (e) {
return Response.error(e.toString());
}
}
private getFileName(format: ExportFormat, prefix?: string) {
if (format === "encrypted_json") {
if (prefix == null) {
prefix = "encrypted";
} else {
prefix = "encrypted_" + prefix;
}
format = "json";
}
return this.exportService.getFileName(prefix, format);
}
private async promptPassword(password: string | boolean) {
// boolean => flag set with no value, we need to prompt for password
// string => flag set with value, use this value for password
// undefined/null/false => account protect, not password, no password needed
if (typeof password === "string") {
return password;
} else if (password) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Export file password:",
});
return answer.password as string;
}
return null;
}
}

View File

@ -0,0 +1,80 @@
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "../utils";
export class GenerateCommand {
constructor(
private passwordGenerationService: PasswordGenerationService,
private stateService: StateService
) {}
async run(cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
const options = {
uppercase: normalizedOptions.uppercase,
lowercase: normalizedOptions.lowercase,
number: normalizedOptions.number,
special: normalizedOptions.special,
length: normalizedOptions.length,
type: normalizedOptions.type,
wordSeparator: normalizedOptions.separator,
numWords: normalizedOptions.words,
capitalize: normalizedOptions.capitalize,
includeNumber: normalizedOptions.includeNumber,
};
const enforcedOptions = (await this.stateService.getIsAuthenticated())
? (await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options))[0]
: options;
const password = await this.passwordGenerationService.generatePassword(enforcedOptions);
const res = new StringResponse(password);
return Response.success(res);
}
}
class Options {
uppercase: boolean;
lowercase: boolean;
number: boolean;
special: boolean;
length: number;
type: "passphrase" | "password";
separator: string;
words: number;
capitalize: boolean;
includeNumber: boolean;
constructor(passedOptions: Record<string, any>) {
this.uppercase = CliUtils.convertBooleanOption(passedOptions?.uppercase);
this.lowercase = CliUtils.convertBooleanOption(passedOptions?.lowercase);
this.number = CliUtils.convertBooleanOption(passedOptions?.number);
this.special = CliUtils.convertBooleanOption(passedOptions?.special);
this.capitalize = CliUtils.convertBooleanOption(passedOptions?.capitalize);
this.includeNumber = CliUtils.convertBooleanOption(passedOptions?.includeNumber);
this.length = passedOptions?.length != null ? parseInt(passedOptions?.length, null) : 14;
this.type = passedOptions?.passphrase ? "passphrase" : "password";
this.separator = passedOptions?.separator == null ? "-" : passedOptions.separator + "";
this.words = passedOptions?.words != null ? parseInt(passedOptions.words, null) : 3;
if (!this.uppercase && !this.lowercase && !this.special && !this.number) {
this.lowercase = true;
this.uppercase = true;
this.number = true;
}
if (this.length < 5) {
this.length = 5;
}
if (this.words < 3) {
this.words = 3;
}
if (this.separator === "space") {
this.separator = " ";
} else if (this.separator != null && this.separator.length > 1) {
this.separator = this.separator[0];
}
}
}

View File

@ -0,0 +1,540 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuditService } from "jslib-common/abstractions/audit.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { SendType } from "jslib-common/enums/sendType";
import { Utils } from "jslib-common/misc/utils";
import { EncString } from "jslib-common/models/domain/encString";
import { Organization } from "jslib-common/models/domain/organization";
import { CardExport } from "jslib-common/models/export/cardExport";
import { CipherExport } from "jslib-common/models/export/cipherExport";
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { FieldExport } from "jslib-common/models/export/fieldExport";
import { FolderExport } from "jslib-common/models/export/folderExport";
import { IdentityExport } from "jslib-common/models/export/identityExport";
import { LoginExport } from "jslib-common/models/export/loginExport";
import { LoginUriExport } from "jslib-common/models/export/loginUriExport";
import { SecureNoteExport } from "jslib-common/models/export/secureNoteExport";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionResponse } from "../models/response/collectionResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { OrganizationResponse } from "../models/response/organizationResponse";
import { SendResponse } from "../models/response/sendResponse";
import { TemplateResponse } from "../models/response/templateResponse";
import { SelectionReadOnly } from "../models/selectionReadOnly";
import { CliUtils } from "../utils";
import { DownloadCommand } from "./download.command";
export class GetCommand extends DownloadCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private totpService: TotpService,
private auditService: AuditService,
cryptoService: CryptoService,
private stateService: StateService,
private searchService: SearchService,
private apiService: ApiService,
private organizationService: OrganizationService
) {
super(cryptoService);
}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "item":
return await this.getCipher(id);
case "username":
return await this.getUsername(id);
case "password":
return await this.getPassword(id);
case "uri":
return await this.getUri(id);
case "totp":
return await this.getTotp(id);
case "notes":
return await this.getNotes(id);
case "exposed":
return await this.getExposed(id);
case "attachment":
return await this.getAttachment(id, normalizedOptions);
case "folder":
return await this.getFolder(id);
case "collection":
return await this.getCollection(id);
case "org-collection":
return await this.getOrganizationCollection(id, normalizedOptions);
case "organization":
return await this.getOrganization(id);
case "template":
return await this.getTemplate(id);
case "fingerprint":
return await this.getFingerprint(id);
default:
return Response.badRequest("Unknown object.");
}
}
private async getCipherView(id: string): Promise<CipherView | CipherView[]> {
let decCipher: CipherView = null;
if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id);
if (cipher != null) {
decCipher = await cipher.decrypt();
}
} else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted();
ciphers = this.searchService.searchCiphersBasic(ciphers, id);
if (ciphers.length > 1) {
return ciphers;
}
if (ciphers.length > 0) {
decCipher = ciphers[0];
}
}
return decCipher;
}
private async getCipher(id: string, filter?: (c: CipherView) => boolean) {
let decCipher = await this.getCipherView(id);
if (decCipher == null) {
return Response.notFound();
}
if (Array.isArray(decCipher)) {
if (filter != null) {
decCipher = decCipher.filter(filter);
if (decCipher.length === 1) {
decCipher = decCipher[0];
}
}
if (Array.isArray(decCipher)) {
return Response.multipleResults(decCipher.map((c) => c.id));
}
}
const res = new CipherResponse(decCipher);
return Response.success(res);
}
private async getUsername(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.username)
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (Utils.isNullOrWhitespace(cipher.login.username)) {
return Response.error("No username available for this login.");
}
const res = new StringResponse(cipher.login.username);
return Response.success(res);
}
private async getPassword(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.password)
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (Utils.isNullOrWhitespace(cipher.login.password)) {
return Response.error("No password available for this login.");
}
const res = new StringResponse(cipher.login.password);
return Response.success(res);
}
private async getUri(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) =>
c.type === CipherType.Login &&
c.login.uris != null &&
c.login.uris.length > 0 &&
c.login.uris[0].uri !== ""
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (
cipher.login.uris == null ||
cipher.login.uris.length === 0 ||
cipher.login.uris[0].uri === ""
) {
return Response.error("No uri available for this login.");
}
const res = new StringResponse(cipher.login.uris[0].uri);
return Response.success(res);
}
private async getTotp(id: string) {
const cipherResponse = await this.getCipher(
id,
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.totp)
);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (cipher.type !== CipherType.Login) {
return Response.badRequest("Not a login.");
}
if (Utils.isNullOrWhitespace(cipher.login.totp)) {
return Response.error("No TOTP available for this login.");
}
const totp = await this.totpService.getCode(cipher.login.totp);
if (totp == null) {
return Response.error("Couldn't generate TOTP code.");
}
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id);
if (
originalCipher == null ||
originalCipher.organizationId == null ||
!originalCipher.organizationUseTotp
) {
return Response.error("Premium status is required to use this feature.");
}
}
const res = new StringResponse(totp);
return Response.success(res);
}
private async getNotes(id: string) {
const cipherResponse = await this.getCipher(id, (c) => !Utils.isNullOrWhitespace(c.notes));
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse;
if (Utils.isNullOrWhitespace(cipher.notes)) {
return Response.error("No notes available for this item.");
}
const res = new StringResponse(cipher.notes);
return Response.success(res);
}
private async getExposed(id: string) {
const passwordResponse = await this.getPassword(id);
if (!passwordResponse.success) {
return passwordResponse;
}
const exposedNumber = await this.auditService.passwordLeaked(
(passwordResponse.data as StringResponse).data
);
const res = new StringResponse(exposedNumber.toString());
return Response.success(res);
}
private async getAttachment(id: string, options: Options) {
if (options.itemId == null || options.itemId === "") {
return Response.badRequest("--itemid <itemid> required.");
}
const itemId = options.itemId.toLowerCase();
const cipherResponse = await this.getCipher(itemId);
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = await this.getCipherView(itemId);
if (
cipher == null ||
Array.isArray(cipher) ||
cipher.attachments == null ||
cipher.attachments.length === 0
) {
return Response.error("No attachments available for this item.");
}
let attachments = cipher.attachments.filter(
(a) =>
a.id.toLowerCase() === id ||
(a.fileName != null && a.fileName.toLowerCase().indexOf(id) > -1)
);
if (attachments.length === 0) {
return Response.error("Attachment `" + id + "` was not found.");
}
const exactMatches = attachments.filter((a) => a.fileName.toLowerCase() === id);
if (exactMatches.length === 1) {
attachments = exactMatches;
}
if (attachments.length > 1) {
return Response.multipleResults(attachments.map((a) => a.id));
}
if (!(await this.stateService.getCanAccessPremium())) {
const originalCipher = await this.cipherService.get(cipher.id);
if (originalCipher == null || originalCipher.organizationId == null) {
return Response.error("Premium status is required to use this feature.");
}
}
let url: string;
try {
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
cipher.id,
attachments[0].id
);
url = attachmentDownloadResponse.url;
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = attachments[0].url;
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
const key =
attachments[0].key != null
? attachments[0].key
: await this.cryptoService.getOrgKey(cipher.organizationId);
return await this.saveAttachmentToFile(url, key, attachments[0].fileName, options.output);
}
private async getFolder(id: string) {
let decFolder: FolderView = null;
if (Utils.isGuid(id)) {
const folder = await this.folderService.get(id);
if (folder != null) {
decFolder = await folder.decrypt();
}
} else if (id.trim() !== "") {
let folders = await this.folderService.getAllDecrypted();
folders = CliUtils.searchFolders(folders, id);
if (folders.length > 1) {
return Response.multipleResults(folders.map((f) => f.id));
}
if (folders.length > 0) {
decFolder = folders[0];
}
}
if (decFolder == null) {
return Response.notFound();
}
const res = new FolderResponse(decFolder);
return Response.success(res);
}
private async getCollection(id: string) {
let decCollection: CollectionView = null;
if (Utils.isGuid(id)) {
const collection = await this.collectionService.get(id);
if (collection != null) {
decCollection = await collection.decrypt();
}
} else if (id.trim() !== "") {
let collections = await this.collectionService.getAllDecrypted();
collections = CliUtils.searchCollections(collections, id);
if (collections.length > 1) {
return Response.multipleResults(collections.map((c) => c.id));
}
if (collections.length > 0) {
decCollection = collections[0];
}
}
if (decCollection == null) {
return Response.notFound();
}
const res = new CollectionResponse(decCollection);
return Response.success(res);
}
private async getOrganizationCollection(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) {
throw new Error("No encryption key for this organization.");
}
const response = await this.apiService.getCollectionDetails(options.organizationId, id);
const decCollection = new CollectionView(response);
decCollection.name = await this.cryptoService.decryptToUtf8(
new EncString(response.name),
orgKey
);
const groups =
response.groups == null
? null
: response.groups.map((g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords));
const res = new OrganizationCollectionResponse(decCollection, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async getOrganization(id: string) {
let org: Organization = null;
if (Utils.isGuid(id)) {
org = await this.organizationService.get(id);
} else if (id.trim() !== "") {
let orgs = await this.organizationService.getAll();
orgs = CliUtils.searchOrganizations(orgs, id);
if (orgs.length > 1) {
return Response.multipleResults(orgs.map((c) => c.id));
}
if (orgs.length > 0) {
org = orgs[0];
}
}
if (org == null) {
return Response.notFound();
}
const res = new OrganizationResponse(org);
return Response.success(res);
}
private async getTemplate(id: string) {
let template: any = null;
switch (id.toLowerCase()) {
case "item":
template = CipherExport.template();
break;
case "item.field":
template = FieldExport.template();
break;
case "item.login":
template = LoginExport.template();
break;
case "item.login.uri":
template = LoginUriExport.template();
break;
case "item.card":
template = CardExport.template();
break;
case "item.identity":
template = IdentityExport.template();
break;
case "item.securenote":
template = SecureNoteExport.template();
break;
case "folder":
template = FolderExport.template();
break;
case "collection":
template = CollectionExport.template();
break;
case "item-collections":
template = ["collection-id1", "collection-id2"];
break;
case "org-collection":
template = OrganizationCollectionRequest.template();
break;
case "send.text":
template = SendResponse.template(SendType.Text);
break;
case "send.file":
template = SendResponse.template(SendType.File);
break;
default:
return Response.badRequest("Unknown template object.");
}
const res = new TemplateResponse(template);
return Response.success(res);
}
private async getFingerprint(id: string) {
let fingerprint: string[] = null;
if (id === "me") {
fingerprint = await this.cryptoService.getFingerprint(await this.stateService.getUserId());
} else if (Utils.isGuid(id)) {
try {
const response = await this.apiService.getUserPublicKey(id);
const pubKey = Utils.fromB64ToArray(response.publicKey);
fingerprint = await this.cryptoService.getFingerprint(id, pubKey.buffer);
} catch {
// eslint-disable-next-line
}
}
if (fingerprint == null) {
return Response.notFound();
}
const res = new StringResponse(fingerprint.join("-"));
return Response.success(res);
}
}
class Options {
itemId: string;
organizationId: string;
output: string;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
this.output = passedOptions?.output;
}
}

View File

@ -0,0 +1,127 @@
import * as program from "commander";
import * as inquirer from "inquirer";
import { ImportService } from "jslib-common/abstractions/import.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { ImportType } from "jslib-common/enums/importOptions";
import { Importer } from "jslib-common/importers/importer";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { CliUtils } from "../utils";
export class ImportCommand {
constructor(
private importService: ImportService,
private organizationService: OrganizationService
) {}
async run(
format: ImportType,
filepath: string,
options: program.OptionValues
): Promise<Response> {
const organizationId = options.organizationid;
if (organizationId != null) {
const organization = await this.organizationService.get(organizationId);
if (organization == null) {
return Response.badRequest(
`You do not belong to an organization with the ID of ${organizationId}. Check the organization ID and sync your vault.`
);
}
if (!organization.canAccessImportExport) {
return Response.badRequest(
"You are not authorized to import into the provided organization."
);
}
}
if (options.formats || false) {
return await this.list();
} else {
return await this.import(format, filepath, organizationId);
}
}
private async import(format: ImportType, filepath: string, organizationId: string) {
if (format == null) {
return Response.badRequest("`format` was not provided.");
}
if (filepath == null || filepath === "") {
return Response.badRequest("`filepath` was not provided.");
}
const importer = await this.importService.getImporter(format, organizationId);
if (importer === null) {
return Response.badRequest("Proper importer type required.");
}
try {
let contents;
if (format === "1password1pux") {
contents = await CliUtils.extract1PuxContent(filepath);
} else {
contents = await CliUtils.readFile(filepath);
}
if (contents === null || contents === "") {
return Response.badRequest("Import file was empty.");
}
const response = await this.doImport(importer, contents, organizationId);
if (response.success) {
response.data = new MessageResponse("Imported " + filepath, null);
}
return response;
} catch (err) {
return Response.badRequest(err);
}
}
private async list() {
const options = this.importService
.getImportOptions()
.sort((a, b) => {
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
})
.map((option) => option.id)
.join("\n");
const res = new MessageResponse("Supported input formats:", options);
res.raw = options;
return Response.success(res);
}
private async doImport(
importer: Importer,
contents: string,
organizationId?: string
): Promise<Response> {
const err = await this.importService.import(importer, contents, organizationId);
if (err != null) {
if (err.passwordRequired) {
importer = this.importService.getImporter(
"bitwardenpasswordprotected",
organizationId,
await this.promptPassword()
);
return this.doImport(importer, contents, organizationId);
}
return Response.badRequest(err.message);
}
return Response.success();
}
private async promptPassword() {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Import file password:",
});
return answer.password;
}
}

View File

@ -0,0 +1,252 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { Utils } from "jslib-common/misc/utils";
import { CollectionData } from "jslib-common/models/data/collectionData";
import { Collection } from "jslib-common/models/domain/collection";
import {
CollectionDetailsResponse as ApiCollectionDetailsResponse,
CollectionResponse as ApiCollectionResponse,
} from "jslib-common/models/response/collectionResponse";
import { ListResponse as ApiListResponse } from "jslib-common/models/response/listResponse";
import { CipherView } from "jslib-common/models/view/cipherView";
import { Response } from "jslib-node/cli/models/response";
import { ListResponse } from "jslib-node/cli/models/response/listResponse";
import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionResponse } from "../models/response/collectionResponse";
import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationResponse } from "../models/response/organizationResponse";
import { OrganizationUserResponse } from "../models/response/organizationUserResponse";
import { CliUtils } from "../utils";
export class ListCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
private searchService: SearchService,
private apiService: ApiService
) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
switch (object.toLowerCase()) {
case "items":
return await this.listCiphers(normalizedOptions);
case "folders":
return await this.listFolders(normalizedOptions);
case "collections":
return await this.listCollections(normalizedOptions);
case "org-collections":
return await this.listOrganizationCollections(normalizedOptions);
case "org-members":
return await this.listOrganizationMembers(normalizedOptions);
case "organizations":
return await this.listOrganizations(normalizedOptions);
default:
return Response.badRequest("Unknown object.");
}
}
private async listCiphers(options: Options) {
let ciphers: CipherView[];
options.trash = options.trash || false;
if (options.url != null && options.url.trim() !== "") {
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url);
} else {
ciphers = await this.cipherService.getAllDecrypted();
}
if (
options.folderId != null ||
options.collectionId != null ||
options.organizationId != null
) {
ciphers = ciphers.filter((c) => {
if (options.trash !== c.isDeleted) {
return false;
}
if (options.folderId != null) {
if (options.folderId === "notnull" && c.folderId != null) {
return true;
}
const folderId = options.folderId === "null" ? null : options.folderId;
if (folderId === c.folderId) {
return true;
}
}
if (options.organizationId != null) {
if (options.organizationId === "notnull" && c.organizationId != null) {
return true;
}
const organizationId = options.organizationId === "null" ? null : options.organizationId;
if (organizationId === c.organizationId) {
return true;
}
}
if (options.collectionId != null) {
if (
options.collectionId === "notnull" &&
c.collectionIds != null &&
c.collectionIds.length > 0
) {
return true;
}
const collectionId = options.collectionId === "null" ? null : options.collectionId;
if (collectionId == null && (c.collectionIds == null || c.collectionIds.length === 0)) {
return true;
}
if (
collectionId != null &&
c.collectionIds != null &&
c.collectionIds.indexOf(collectionId) > -1
) {
return true;
}
}
return false;
});
} else if (options.search == null || options.search.trim() === "") {
ciphers = ciphers.filter((c) => options.trash === c.isDeleted);
}
if (options.search != null && options.search.trim() !== "") {
ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash);
}
const res = new ListResponse(ciphers.map((o) => new CipherResponse(o)));
return Response.success(res);
}
private async listFolders(options: Options) {
let folders = await this.folderService.getAllDecrypted();
if (options.search != null && options.search.trim() !== "") {
folders = CliUtils.searchFolders(folders, options.search);
}
const res = new ListResponse(folders.map((o) => new FolderResponse(o)));
return Response.success(res);
}
private async listCollections(options: Options) {
let collections = await this.collectionService.getAllDecrypted();
if (options.organizationId != null) {
collections = collections.filter((c) => {
if (options.organizationId === c.organizationId) {
return true;
}
return false;
});
}
if (options.search != null && options.search.trim() !== "") {
collections = CliUtils.searchCollections(collections, options.search);
}
const res = new ListResponse(collections.map((o) => new CollectionResponse(o)));
return Response.success(res);
}
private async listOrganizationCollections(options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
const organization = await this.organizationService.get(options.organizationId);
if (organization == null) {
return Response.error("Organization not found.");
}
try {
let response: ApiListResponse<ApiCollectionResponse>;
if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(options.organizationId);
} else {
response = await this.apiService.getUserCollections();
}
const collections = response.data
.filter((c) => c.organizationId === options.organizationId)
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
let decCollections = await this.collectionService.decryptMany(collections);
if (options.search != null && options.search.trim() !== "") {
decCollections = CliUtils.searchCollections(decCollections, options.search);
}
const res = new ListResponse(decCollections.map((o) => new CollectionResponse(o)));
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async listOrganizationMembers(options: Options) {
if (options.organizationId == null || options.organizationId === "") {
return Response.badRequest("`organizationid` option is required.");
}
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
}
const organization = await this.organizationService.get(options.organizationId);
if (organization == null) {
return Response.error("Organization not found.");
}
try {
const response = await this.apiService.getOrganizationUsers(options.organizationId);
const res = new ListResponse(
response.data.map((r) => {
const u = new OrganizationUserResponse();
u.email = r.email;
u.name = r.name;
u.id = r.id;
u.status = r.status;
u.type = r.type;
u.twoFactorEnabled = r.twoFactorEnabled;
return u;
})
);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async listOrganizations(options: Options) {
let organizations = await this.organizationService.getAll();
if (options.search != null && options.search.trim() !== "") {
organizations = CliUtils.searchOrganizations(organizations, options.search);
}
const res = new ListResponse(organizations.map((o) => new OrganizationResponse(o)));
return Response.success(res);
}
}
class Options {
organizationId: string;
collectionId: string;
folderId: string;
search: string;
url: string;
trash: boolean;
constructor(passedOptions: Record<string, any>) {
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
this.collectionId = passedOptions?.collectionid || passedOptions?.collectionId;
this.folderId = passedOptions?.folderid || passedOptions?.folderId;
this.search = passedOptions?.search;
this.url = passedOptions?.url;
this.trash = CliUtils.convertBooleanOption(passedOptions?.trash);
}
}

View File

@ -0,0 +1,14 @@
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class LockCommand {
constructor(private vaultTimeoutService: VaultTimeoutService) {}
async run() {
await this.vaultTimeoutService.lock();
process.env.BW_SESSION = null;
const res = new MessageResponse("Your vault is locked.", null);
return Response.success(res);
}
}

View File

@ -0,0 +1,99 @@
import * as program from "commander";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { Utils } from "jslib-common/misc/utils";
import { LoginCommand as BaseLoginCommand } from "jslib-node/cli/commands/login.command";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
export class LoginCommand extends BaseLoginCommand {
private options: program.OptionValues;
constructor(
authService: AuthService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
i18nService: I18nService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
stateService: StateService,
cryptoService: CryptoService,
policyService: PolicyService,
twoFactorService: TwoFactorService,
private syncService: SyncService,
private keyConnectorService: KeyConnectorService,
private logoutCallback: () => Promise<void>
) {
super(
authService,
apiService,
i18nService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
platformUtilsService,
stateService,
cryptoService,
policyService,
twoFactorService,
"cli"
);
this.logout = this.logoutCallback;
this.validatedParams = async () => {
const key = await cryptoFunctionService.randomBytes(64);
process.env.BW_SESSION = Utils.fromBufferToB64(key);
};
this.success = async () => {
await this.syncService.fullSync(true);
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
if (
(this.options.sso != null || this.options.apikey != null) &&
this.canInteract &&
!usesKeyConnector
) {
const res = new MessageResponse(
"You are logged in!",
"\n" + "To unlock your vault, use the `unlock` command. ex:\n" + "$ bw unlock"
);
return res;
} else {
const res = new MessageResponse(
"You are logged in!",
"\n" +
"To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:\n" +
'$ export BW_SESSION="' +
process.env.BW_SESSION +
'"\n' +
'> $env:BW_SESSION="' +
process.env.BW_SESSION +
'"\n\n' +
"You can also pass the session key to any command with the `--session` option. ex:\n" +
"$ bw list items --session " +
process.env.BW_SESSION
);
res.raw = process.env.BW_SESSION;
return res;
}
};
}
run(email: string, password: string, options: program.OptionValues) {
this.options = options;
this.email = email;
return super.run(email, password, options);
}
}

View File

@ -0,0 +1,36 @@
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { Response } from "jslib-node/cli/models/response";
export class RestoreCommand {
constructor(private cipherService: CipherService) {}
async run(object: string, id: string): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
switch (object.toLowerCase()) {
case "item":
return await this.restoreCipher(id);
default:
return Response.badRequest("Unknown object.");
}
}
private async restoreCipher(id: string) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
if (cipher.deletedDate == null) {
return Response.badRequest("Cipher is not in trash.");
}
try {
await this.cipherService.restoreWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}

View File

@ -0,0 +1,149 @@
import * as fs from "fs";
import * as path from "path";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SendType } from "jslib-common/enums/sendType";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
import { SendTextResponse } from "../../models/response/sendTextResponse";
import { CliUtils } from "../../utils";
export class SendCreateCommand {
constructor(
private sendService: SendService,
private stateService: StateService,
private environmentService: EnvironmentService
) {}
async run(requestJson: any, cmdOptions: Record<string, any>) {
let req: any = null;
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
if (typeof requestJson !== "string") {
req = requestJson;
req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate);
req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate);
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
if (req == null) {
throw new Error("Null request");
}
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
if (
req.deletionDate == null ||
isNaN(new Date(req.deletionDate).getTime()) ||
new Date(req.deletionDate) <= new Date()
) {
return Response.badRequest("Must specify a valid deletion date after the current time");
}
if (req.expirationDate != null && isNaN(new Date(req.expirationDate).getTime())) {
return Response.badRequest("Unable to parse expirationDate: " + req.expirationDate);
}
const normalizedOptions = new Options(cmdOptions);
return this.createSend(req, normalizedOptions);
}
private async createSend(req: SendResponse, options: Options) {
const filePath = req.file?.fileName ?? options.file;
const text = req.text?.text ?? options.text;
const hidden = req.text?.hidden ?? options.hidden;
const password = req.password ?? options.password;
const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount;
req.key = null;
req.maxAccessCount = maxAccessCount;
switch (req.type) {
case SendType.File:
if (process.env.BW_SERVE === "true") {
return Response.error(
"Creating a file-based Send is unsupported through the `serve` command at this time."
);
}
if (!(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
if (filePath == null) {
return Response.badRequest(
"Must specify a file to Send either with the --file option or in the request JSON."
);
}
req.file.fileName = path.basename(filePath);
break;
case SendType.Text:
if (text == null) {
return Response.badRequest(
"Must specify text content to Send either with the --text option or in the request JSON."
);
}
req.text = new SendTextResponse();
req.text.text = text;
req.text.hidden = hidden;
break;
default:
return Response.badRequest(
"Unknown Send type " + SendType[req.type] + ". Valid types are: file, text"
);
}
try {
let fileBuffer: ArrayBuffer = null;
if (req.type === SendType.File) {
fileBuffer = NodeUtils.bufferToArrayBuffer(fs.readFileSync(filePath));
}
const sendView = SendResponse.toView(req);
const [encSend, fileData] = await this.sendService.encrypt(sendView, fileBuffer, password);
// Add dates from template
encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
await this.sendService.saveWithServer([encSend, fileData]);
const newSend = await this.sendService.get(encSend.id);
const decSend = await newSend.decrypt();
const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl());
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}
class Options {
file: string;
text: string;
maxAccessCount: number;
password: string;
hidden: boolean;
constructor(passedOptions: Record<string, any>) {
this.file = passedOptions?.file;
this.text = passedOptions?.text;
this.password = passedOptions?.password;
this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden);
this.maxAccessCount =
passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null;
}
}

View File

@ -0,0 +1,21 @@
import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from "jslib-node/cli/models/response";
export class SendDeleteCommand {
constructor(private sendService: SendService) {}
async run(id: string) {
const send = await this.sendService.get(id);
if (send == null) {
return Response.notFound();
}
try {
await this.sendService.deleteWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
}

View File

@ -0,0 +1,90 @@
import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SendType } from "jslib-common/enums/sendType";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
import { CliUtils } from "../../utils";
import { SendGetCommand } from "./get.command";
export class SendEditCommand {
constructor(
private sendService: SendService,
private stateService: StateService,
private getCommand: SendGetCommand
) {}
async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
let req: SendResponse = null;
if (typeof requestJson !== "string") {
req = requestJson;
req.deletionDate = req.deletionDate == null ? null : new Date(req.deletionDate);
req.expirationDate = req.expirationDate == null ? null : new Date(req.expirationDate);
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
const normalizedOptions = new Options(cmdOptions);
req.id = normalizedOptions.itemId || req.id;
if (req.id != null) {
req.id = req.id.toLowerCase();
}
const send = await this.sendService.get(req.id);
if (send == null) {
return Response.notFound();
}
if (send.type !== req.type) {
return Response.badRequest("Cannot change a Send's type");
}
if (send.type === SendType.File && !(await this.stateService.getCanAccessPremium())) {
return Response.error("Premium status is required to use this feature.");
}
let sendView = await send.decrypt();
sendView = SendResponse.toView(req, sendView);
if (typeof req.password !== "string" || req.password === "") {
req.password = null;
}
try {
const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password);
// Add dates from template
encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
await this.sendService.saveWithServer([encSend, encFileData]);
} catch (e) {
return Response.error(e);
}
return await this.getCommand.run(send.id, {});
}
}
class Options {
itemId: string;
constructor(passedOptions: Record<string, any>) {
this.itemId = passedOptions?.itemId || passedOptions?.itemid;
}
}

View File

@ -0,0 +1,83 @@
import * as program from "commander";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { Utils } from "jslib-common/misc/utils";
import { SendView } from "jslib-common/models/view/sendView";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
import { DownloadCommand } from "../download.command";
export class SendGetCommand extends DownloadCommand {
constructor(
private sendService: SendService,
private environmentService: EnvironmentService,
private searchService: SearchService,
cryptoService: CryptoService
) {
super(cryptoService);
}
async run(id: string, options: program.OptionValues) {
const serveCommand = process.env.BW_SERVE === "true";
if (serveCommand && !Utils.isGuid(id)) {
return Response.badRequest("`" + id + "` is not a GUID.");
}
let sends = await this.getSendView(id);
if (sends == null) {
return Response.notFound();
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
let filter = (s: SendView) => true;
let selector = async (s: SendView): Promise<Response> =>
Response.success(new SendResponse(s, webVaultUrl));
if (!serveCommand && options?.text != null) {
filter = (s) => {
return filter(s) && s.text != null;
};
selector = async (s) => {
// Write to stdout and response success so we get the text string only to stdout
process.stdout.write(s.text.text);
return Response.success();
};
}
if (Array.isArray(sends)) {
if (filter != null) {
sends = sends.filter(filter);
}
if (sends.length > 1) {
return Response.multipleResults(sends.map((s) => s.id));
}
if (sends.length > 0) {
return selector(sends[0]);
} else {
return Response.notFound();
}
}
return selector(sends);
}
private async getSendView(id: string): Promise<SendView | SendView[]> {
if (Utils.isGuid(id)) {
const send = await this.sendService.get(id);
if (send != null) {
return await send.decrypt();
}
} else if (id.trim() !== "") {
let sends = await this.sendService.getAllDecrypted();
sends = this.searchService.searchSends(sends, id);
if (sends.length > 1) {
return sends;
} else if (sends.length > 0) {
return sends[0];
}
}
}
}

View File

@ -0,0 +1,36 @@
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from "jslib-node/cli/models/response";
import { ListResponse } from "jslib-node/cli/models/response/listResponse";
import { SendResponse } from "../..//models/response/sendResponse";
export class SendListCommand {
constructor(
private sendService: SendService,
private environmentService: EnvironmentService,
private searchService: SearchService
) {}
async run(cmdOptions: Record<string, any>): Promise<Response> {
let sends = await this.sendService.getAllDecrypted();
const normalizedOptions = new Options(cmdOptions);
if (normalizedOptions.search != null && normalizedOptions.search.trim() !== "") {
sends = this.searchService.searchSends(sends, normalizedOptions.search);
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
const res = new ListResponse(sends.map((s) => new SendResponse(s, webVaultUrl)));
return Response.success(res);
}
}
class Options {
search: string;
constructor(passedOptions: Record<string, any>) {
this.search = passedOptions?.search;
}
}

View File

@ -0,0 +1,170 @@
import * as program from "commander";
import * as inquirer from "inquirer";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SendType } from "jslib-common/enums/sendType";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Utils } from "jslib-common/misc/utils";
import { SendAccess } from "jslib-common/models/domain/sendAccess";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { SendAccessRequest } from "jslib-common/models/request/sendAccessRequest";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { SendAccessView } from "jslib-common/models/view/sendAccessView";
import { Response } from "jslib-node/cli/models/response";
import { SendAccessResponse } from "../../models/response/sendAccessResponse";
import { DownloadCommand } from "../download.command";
export class SendReceiveCommand extends DownloadCommand {
private canInteract: boolean;
private decKey: SymmetricCryptoKey;
private sendAccessRequest: SendAccessRequest;
constructor(
private apiService: ApiService,
cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService
) {
super(cryptoService);
}
async run(url: string, options: program.OptionValues): Promise<Response> {
this.canInteract = process.env.BW_NOINTERACTION !== "true";
let urlObject: URL;
try {
urlObject = new URL(url);
} catch (e) {
return Response.badRequest("Failed to parse the provided Send url");
}
const apiUrl = this.getApiUrl(urlObject);
const [id, key] = this.getIdAndKey(urlObject);
if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
return Response.badRequest("Failed to parse url, the url provided is not a valid Send url");
}
const keyArray = Utils.fromUrlB64ToArray(key);
this.sendAccessRequest = new SendAccessRequest();
let password = options.password;
if (password == null || password === "") {
if (options.passwordfile) {
password = await NodeUtils.readFirstLine(options.passwordfile);
} else if (options.passwordenv && process.env[options.passwordenv]) {
password = process.env[options.passwordenv];
}
}
if (password != null && password !== "") {
this.sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray);
}
const response = await this.sendRequest(apiUrl, id, keyArray);
if (response instanceof Response) {
// Error scenario
return response;
}
if (options.obj != null) {
return Response.success(new SendAccessResponse(response));
}
switch (response.type) {
case SendType.Text:
// Write to stdout and response success so we get the text string only to stdout
process.stdout.write(response?.text?.text);
return Response.success();
case SendType.File: {
const downloadData = await this.apiService.getSendFileDownloadData(
response,
this.sendAccessRequest,
apiUrl
);
return await this.saveAttachmentToFile(
downloadData.url,
this.decKey,
response?.file?.fileName,
options.output
);
}
default:
return Response.success(new SendAccessResponse(response));
}
}
private getIdAndKey(url: URL): [string, string] {
const result = url.hash.slice(1).split("/").slice(-2);
return [result[0], result[1]];
}
private getApiUrl(url: URL) {
const urls = this.environmentService.getUrls();
if (url.origin === "https://send.bitwarden.com") {
return "https://vault.bitwarden.com/api";
} else if (url.origin === urls.api) {
return url.origin;
} else if (this.platformUtilsService.isDev() && url.origin === urls.webVault) {
return urls.api;
} else {
return url.origin + "/api";
}
}
private async getUnlockedPassword(password: string, keyArray: ArrayBuffer) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(
password,
keyArray,
"sha256",
100000
);
return Utils.fromBufferToB64(passwordHash);
}
private async sendRequest(
url: string,
id: string,
key: ArrayBuffer
): Promise<Response | SendAccessView> {
try {
const sendResponse = await this.apiService.postSendAccess(id, this.sendAccessRequest, url);
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.cryptoService.makeSendKey(key);
return await sendAccess.decrypt(this.decKey);
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Send password:",
});
// reattempt with new password
this.sendAccessRequest.password = await this.getUnlockedPassword(answer.password, key);
return await this.sendRequest(url, id, key);
}
return Response.badRequest("Incorrect or missing password");
} else if (e.statusCode === 405) {
return Response.badRequest("Bad Request");
} else if (e.statusCode === 404) {
return Response.notFound();
}
}
return Response.error(e);
}
}
}

View File

@ -0,0 +1,21 @@
import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from "../../models/response/sendResponse";
export class SendRemovePasswordCommand {
constructor(private sendService: SendService) {}
async run(id: string) {
try {
await this.sendService.removePasswordWithServer(id);
const updatedSend = await this.sendService.get(id);
const decSend = await updatedSend.decrypt();
const res = new SendResponse(decSend);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@ -0,0 +1,394 @@
import * as koaMulter from "@koa/multer";
import * as koaRouter from "@koa/router";
import * as program from "commander";
import * as koa from "koa";
import * as koaBodyParser from "koa-bodyparser";
import * as koaJson from "koa-json";
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
import { Response } from "jslib-node/cli/models/response";
import { FileResponse } from "jslib-node/cli/models/response/fileResponse";
import { Main } from "../bw";
import { ConfirmCommand } from "./confirm.command";
import { CreateCommand } from "./create.command";
import { DeleteCommand } from "./delete.command";
import { EditCommand } from "./edit.command";
import { GenerateCommand } from "./generate.command";
import { GetCommand } from "./get.command";
import { ListCommand } from "./list.command";
import { LockCommand } from "./lock.command";
import { RestoreCommand } from "./restore.command";
import { SendCreateCommand } from "./send/create.command";
import { SendDeleteCommand } from "./send/delete.command";
import { SendEditCommand } from "./send/edit.command";
import { SendGetCommand } from "./send/get.command";
import { SendListCommand } from "./send/list.command";
import { SendRemovePasswordCommand } from "./send/removePassword.command";
import { ShareCommand } from "./share.command";
import { StatusCommand } from "./status.command";
import { SyncCommand } from "./sync.command";
import { UnlockCommand } from "./unlock.command";
export class ServeCommand {
private listCommand: ListCommand;
private getCommand: GetCommand;
private createCommand: CreateCommand;
private editCommand: EditCommand;
private generateCommand: GenerateCommand;
private shareCommand: ShareCommand;
private statusCommand: StatusCommand;
private syncCommand: SyncCommand;
private deleteCommand: DeleteCommand;
private confirmCommand: ConfirmCommand;
private restoreCommand: RestoreCommand;
private lockCommand: LockCommand;
private unlockCommand: UnlockCommand;
private sendCreateCommand: SendCreateCommand;
private sendDeleteCommand: SendDeleteCommand;
private sendEditCommand: SendEditCommand;
private sendGetCommand: SendGetCommand;
private sendListCommand: SendListCommand;
private sendRemovePasswordCommand: SendRemovePasswordCommand;
constructor(protected main: Main) {
this.getCommand = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
this.listCommand = new ListCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.organizationService,
this.main.searchService,
this.main.apiService
);
this.createCommand = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService
);
this.editCommand = new EditCommand(
this.main.cipherService,
this.main.folderService,
this.main.cryptoService,
this.main.apiService
);
this.generateCommand = new GenerateCommand(
this.main.passwordGenerationService,
this.main.stateService
);
this.syncCommand = new SyncCommand(this.main.syncService);
this.statusCommand = new StatusCommand(
this.main.environmentService,
this.main.syncService,
this.main.stateService,
this.main.authService
);
this.deleteCommand = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService
);
this.confirmCommand = new ConfirmCommand(this.main.apiService, this.main.cryptoService);
this.restoreCommand = new RestoreCommand(this.main.cipherService);
this.shareCommand = new ShareCommand(this.main.cipherService);
this.lockCommand = new LockCommand(this.main.vaultTimeoutService);
this.unlockCommand = new UnlockCommand(
this.main.cryptoService,
this.main.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService,
this.main.keyConnectorService,
this.main.environmentService,
this.main.syncService,
async () => await this.main.logout()
);
this.sendCreateCommand = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService
);
this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService);
this.sendGetCommand = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
this.sendEditCommand = new SendEditCommand(
this.main.sendService,
this.main.stateService,
this.sendGetCommand
);
this.sendListCommand = new SendListCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService
);
this.sendRemovePasswordCommand = new SendRemovePasswordCommand(this.main.sendService);
}
async run(options: program.OptionValues) {
const port = options.port || 8087;
const hostname = options.hostname || "localhost";
const server = new koa();
const router = new koaRouter();
process.env.BW_SERVE = "true";
process.env.BW_NOINTERACTION = "true";
server.use(koaBodyParser()).use(koaJson({ pretty: false, param: "pretty" }));
router.get("/generate", async (ctx, next) => {
const response = await this.generateCommand.run(ctx.request.query);
this.processResponse(ctx.response, response);
await next();
});
router.get("/status", async (ctx, next) => {
const response = await this.statusCommand.run();
this.processResponse(ctx.response, response);
await next();
});
router.get("/list/object/:object", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendListCommand.run(ctx.request.query);
} else {
response = await this.listCommand.run(ctx.params.object, ctx.request.query);
}
this.processResponse(ctx.response, response);
await next();
});
router.get("/send/list", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.sendListCommand.run(ctx.request.query);
this.processResponse(ctx.response, response);
await next();
});
router.post("/sync", async (ctx, next) => {
const response = await this.syncCommand.run(ctx.request.query);
this.processResponse(ctx.response, response);
await next();
});
router.post("/lock", async (ctx, next) => {
const response = await this.lockCommand.run();
this.processResponse(ctx.response, response);
await next();
});
router.post("/unlock", async (ctx, next) => {
const response = await this.unlockCommand.run(
ctx.request.body.password == null ? null : (ctx.request.body.password as string),
ctx.request.query
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/confirm/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.confirmCommand.run(
ctx.params.object,
ctx.params.id,
ctx.request.query
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/restore/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.restoreCommand.run(ctx.params.object, ctx.params.id);
this.processResponse(ctx.response, response);
await next();
});
router.post("/move/:id/:organizationId", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.shareCommand.run(
ctx.params.id,
ctx.params.organizationId,
ctx.request.body // TODO: Check the format of this body for an array of collection ids
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/attachment", koaMulter().single("file"), async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.createCommand.run(
"attachment",
ctx.request.body,
ctx.request.query,
{
fileBuffer: ctx.request.file.buffer,
fileName: ctx.request.file.originalname,
}
);
this.processResponse(ctx.response, response);
await next();
});
router.post("/send/:id/remove-password", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
const response = await this.sendRemovePasswordCommand.run(ctx.params.id);
this.processResponse(ctx.response, response);
await next();
});
router.post("/object/:object", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendCreateCommand.run(ctx.request.body, ctx.request.query);
} else {
response = await this.createCommand.run(
ctx.params.object,
ctx.request.body,
ctx.request.query
);
}
this.processResponse(ctx.response, response);
await next();
});
router.put("/object/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
ctx.request.body.id = ctx.params.id;
response = await this.sendEditCommand.run(ctx.request.body, ctx.request.query);
} else {
response = await this.editCommand.run(
ctx.params.object,
ctx.params.id,
ctx.request.body,
ctx.request.query
);
}
this.processResponse(ctx.response, response);
await next();
});
router.get("/object/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendGetCommand.run(ctx.params.id, null);
} else {
response = await this.getCommand.run(ctx.params.object, ctx.params.id, ctx.request.query);
}
this.processResponse(ctx.response, response);
await next();
});
router.delete("/object/:object/:id", async (ctx, next) => {
if (await this.errorIfLocked(ctx.response)) {
await next();
return;
}
let response: Response = null;
if (ctx.params.object === "send") {
response = await this.sendDeleteCommand.run(ctx.params.id);
} else {
response = await this.deleteCommand.run(
ctx.params.object,
ctx.params.id,
ctx.request.query
);
}
this.processResponse(ctx.response, response);
await next();
});
server
.use(router.routes())
.use(router.allowedMethods())
.listen(port, hostname === "all" ? null : hostname, () => {
this.main.logService.info("Listening on " + hostname + ":" + port);
});
}
private processResponse(res: koa.Response, commandResponse: Response) {
if (!commandResponse.success) {
res.status = 400;
}
if (commandResponse.data instanceof FileResponse) {
res.body = commandResponse.data.data;
res.attachment(commandResponse.data.fileName);
res.set("Content-Type", "application/octet-stream");
res.set("Content-Length", commandResponse.data.data.length.toString());
} else {
res.body = commandResponse;
}
}
private async errorIfLocked(res: koa.Response) {
const authed = await this.main.stateService.getIsAuthenticated();
if (!authed) {
this.processResponse(res, Response.error("You are not logged in."));
return true;
}
if (await this.main.cryptoService.hasKeyInMemory()) {
return false;
} else if (await this.main.cryptoService.hasKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
await this.main.cryptoService.getKey();
return false;
}
this.processResponse(res, Response.error("Vault is locked."));
return true;
}
}

View File

@ -0,0 +1,59 @@
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { Response } from "jslib-node/cli/models/response";
import { CipherResponse } from "../models/response/cipherResponse";
import { CliUtils } from "../utils";
export class ShareCommand {
constructor(private cipherService: CipherService) {}
async run(id: string, organizationId: string, requestJson: string): Promise<Response> {
if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === "") {
return Response.badRequest("`requestJson` was not provided.");
}
let req: string[] = [];
if (typeof requestJson !== "string") {
req = requestJson;
} else {
try {
const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson);
if (req == null || req.length === 0) {
return Response.badRequest("You must provide at least one collection id for this item.");
}
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
}
if (id != null) {
id = id.toLowerCase();
}
if (organizationId != null) {
organizationId = organizationId.toLowerCase();
}
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId != null) {
return Response.badRequest("This item already belongs to an organization.");
}
const cipherView = await cipher.decrypt();
try {
await this.cipherService.shareWithServer(cipherView, organizationId, req);
const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt();
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@ -0,0 +1,54 @@
import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { Response } from "jslib-node/cli/models/response";
import { TemplateResponse } from "../models/response/templateResponse";
export class StatusCommand {
constructor(
private envService: EnvironmentService,
private syncService: SyncService,
private stateService: StateService,
private authService: AuthService
) {}
async run(): Promise<Response> {
try {
const baseUrl = this.baseUrl();
const status = await this.status();
const lastSync = await this.syncService.getLastSync();
const userId = await this.stateService.getUserId();
const email = await this.stateService.getEmail();
return Response.success(
new TemplateResponse({
serverUrl: baseUrl,
lastSync: lastSync,
userEmail: email,
userId: userId,
status: status,
})
);
} catch (e) {
return Response.error(e);
}
}
private baseUrl(): string {
return this.envService.getUrls().base;
}
private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> {
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Unlocked) {
return "unlocked";
} else if (authStatus === AuthenticationStatus.Locked) {
return "locked";
} else {
return "unauthenticated";
}
}
}

View File

@ -0,0 +1,41 @@
import { SyncService } from "jslib-common/abstractions/sync.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { CliUtils } from "src/utils";
export class SyncCommand {
constructor(private syncService: SyncService) {}
async run(cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = new Options(cmdOptions);
if (normalizedOptions.last) {
return await this.getLastSync();
}
try {
await this.syncService.fullSync(normalizedOptions.force, true);
const res = new MessageResponse("Syncing complete.", null);
return Response.success(res);
} catch (e) {
return Response.error("Syncing failed: " + e.toString());
}
}
private async getLastSync() {
const lastSyncDate = await this.syncService.getLastSync();
const res = new StringResponse(lastSyncDate == null ? null : lastSyncDate.toISOString());
return Response.success(res);
}
}
class Options {
last: boolean;
force: boolean;
constructor(passedOptions: Record<string, any>) {
this.last = CliUtils.convertBooleanOption(passedOptions?.last);
this.force = CliUtils.convertBooleanOption(passedOptions?.force);
}
}

View File

@ -0,0 +1,132 @@
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { HashPurpose } from "jslib-common/enums/hashPurpose";
import { Utils } from "jslib-common/misc/utils";
import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
import { ConsoleLogService } from "jslib-common/services/consoleLog.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { CliUtils } from "../utils";
import { ConvertToKeyConnectorCommand } from "./convertToKeyConnector.command";
export class UnlockCommand {
constructor(
private cryptoService: CryptoService,
private stateService: StateService,
private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private logService: ConsoleLogService,
private keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
private logout: () => Promise<void>
) {}
async run(password: string, cmdOptions: Record<string, any>) {
const normalizedOptions = new Options(cmdOptions);
const passwordResult = await CliUtils.getPassword(password, normalizedOptions, this.logService);
if (passwordResult instanceof Response) {
return passwordResult;
} else {
password = passwordResult;
}
await this.setNewSessionKey();
const email = await this.stateService.getEmail();
const kdf = await this.stateService.getKdfType();
const kdfIterations = await this.stateService.getKdfIterations();
const key = await this.cryptoService.makeKey(password, email, kdf, kdfIterations);
const storedKeyHash = await this.cryptoService.getKeyHash();
let passwordValid = false;
if (key != null) {
if (storedKeyHash != null) {
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, key);
} else {
const serverKeyHash = await this.cryptoService.hashPassword(
password,
key,
HashPurpose.ServerAuthorization
);
const request = new SecretVerificationRequest();
request.masterPasswordHash = serverKeyHash;
try {
await this.apiService.postAccountVerifyPassword(request);
passwordValid = true;
const localKeyHash = await this.cryptoService.hashPassword(
password,
key,
HashPurpose.LocalAuthorization
);
await this.cryptoService.setKeyHash(localKeyHash);
} catch {
// Ignore
}
}
}
if (passwordValid) {
await this.cryptoService.setKey(key);
if (await this.keyConnectorService.getConvertAccountRequired()) {
const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(
this.apiService,
this.keyConnectorService,
this.environmentService,
this.syncService,
this.logout
);
const convertResponse = await convertToKeyConnectorCommand.run();
if (!convertResponse.success) {
return convertResponse;
}
}
return this.successResponse();
} else {
return Response.error("Invalid master password.");
}
}
private async setNewSessionKey() {
const key = await this.cryptoFunctionService.randomBytes(64);
process.env.BW_SESSION = Utils.fromBufferToB64(key);
}
private async successResponse() {
const res = new MessageResponse(
"Your vault is now unlocked!",
"\n" +
"To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:\n" +
'$ export BW_SESSION="' +
process.env.BW_SESSION +
'"\n' +
'> $env:BW_SESSION="' +
process.env.BW_SESSION +
'"\n\n' +
"You can also pass the session key to any command with the `--session` option. ex:\n" +
"$ bw list items --session " +
process.env.BW_SESSION
);
res.raw = process.env.BW_SESSION;
return Response.success(res);
}
}
class Options {
passwordEnv: string;
passwordFile: string;
constructor(passedOptions: Record<string, any>) {
this.passwordEnv = passedOptions?.passwordenv || passedOptions?.passwordEnv;
this.passwordFile = passedOptions?.passwordfile || passedOptions?.passwordFile;
}
}

5
apps/cli/src/flags.ts Normal file
View File

@ -0,0 +1,5 @@
export type Flags = {
serve?: boolean;
};
export type FlagName = keyof Flags;

View File

@ -0,0 +1,44 @@
{
"bitwarden": {
"message": "Bitwarden"
},
"authenticatorAppTitle": {
"message": "Authenticator App"
},
"yubiKeyTitle": {
"message": "YubiKey OTP Security Key"
},
"emailTitle": {
"message": "Email"
},
"noneFolder": {
"message": "No Folder"
},
"importEncKeyError": {
"message": "Invalid file password."
},
"importPasswordRequired": {
"message": "File is password protected, please provide a decryption password."
},
"importFormatError": {
"message": "Data is not formatted correctly. Please check your import file and try again."
},
"importNothingError": {
"message": "Nothing was imported."
},
"verificationCodeRequired": {
"message": "Verification code is required."
},
"invalidVerificationCode": {
"message": "Invalid verification code."
},
"masterPassRequired": {
"message": "Master password is required."
},
"invalidMasterPassword": {
"message": "Invalid master password."
},
"sessionTimeout": {
"message": "Your session has timed out. Please go back and try logging in again."
}
}

View File

@ -0,0 +1,16 @@
import { CollectionExport } from "jslib-common/models/export/collectionExport";
import { SelectionReadOnly } from "../selectionReadOnly";
export class OrganizationCollectionRequest extends CollectionExport {
static template(): OrganizationCollectionRequest {
const req = new OrganizationCollectionRequest();
req.organizationId = "00000000-0000-0000-0000-000000000000";
req.name = "Collection name";
req.externalId = null;
req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()];
return req;
}
groups: SelectionReadOnly[];
}

View File

@ -0,0 +1,17 @@
import { AttachmentView } from "jslib-common/models/view/attachmentView";
export class AttachmentResponse {
id: string;
fileName: string;
size: string;
sizeName: string;
url: string;
constructor(o: AttachmentView) {
this.id = o.id;
this.fileName = o.fileName;
this.size = o.size;
this.sizeName = o.sizeName;
this.url = o.url;
}
}

View File

@ -0,0 +1,33 @@
import { CipherType } from "jslib-common/enums/cipherType";
import { CipherWithIdExport } from "jslib-common/models/export/cipherWithIdsExport";
import { CipherView } from "jslib-common/models/view/cipherView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { AttachmentResponse } from "./attachmentResponse";
import { LoginResponse } from "./loginResponse";
import { PasswordHistoryResponse } from "./passwordHistoryResponse";
export class CipherResponse extends CipherWithIdExport implements BaseResponse {
object: string;
attachments: AttachmentResponse[];
revisionDate: Date;
deletedDate: Date;
passwordHistory: PasswordHistoryResponse[];
constructor(o: CipherView) {
super();
this.object = "item";
this.build(o);
if (o.attachments != null) {
this.attachments = o.attachments.map((a) => new AttachmentResponse(a));
}
this.revisionDate = o.revisionDate;
this.deletedDate = o.deletedDate;
if (o.passwordHistory != null) {
this.passwordHistory = o.passwordHistory.map((h) => new PasswordHistoryResponse(h));
}
if (o.type === CipherType.Login && o.login != null) {
this.login = new LoginResponse(o.login);
}
}
}

View File

@ -0,0 +1,13 @@
import { CollectionWithIdExport } from "jslib-common/models/export/collectionWithIdExport";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class CollectionResponse extends CollectionWithIdExport implements BaseResponse {
object: string;
constructor(o: CollectionView) {
super();
this.object = "collection";
this.build(o);
}
}

View File

@ -0,0 +1,13 @@
import { FolderWithIdExport } from "jslib-common/models/export/folderWithIdExport";
import { FolderView } from "jslib-common/models/view/folderView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class FolderResponse extends FolderWithIdExport implements BaseResponse {
object: string;
constructor(o: FolderView) {
super();
this.object = "folder";
this.build(o);
}
}

View File

@ -0,0 +1,11 @@
import { LoginExport } from "jslib-common/models/export/loginExport";
import { LoginView } from "jslib-common/models/view/loginView";
export class LoginResponse extends LoginExport {
passwordRevisionDate: Date;
constructor(o: LoginView) {
super(o);
this.passwordRevisionDate = o.passwordRevisionDate != null ? o.passwordRevisionDate : null;
}
}

View File

@ -0,0 +1,15 @@
import { CollectionView } from "jslib-common/models/view/collectionView";
import { SelectionReadOnly } from "../selectionReadOnly";
import { CollectionResponse } from "./collectionResponse";
export class OrganizationCollectionResponse extends CollectionResponse {
groups: SelectionReadOnly[];
constructor(o: CollectionView, groups: SelectionReadOnly[]) {
super(o);
this.object = "org-collection";
this.groups = groups;
}
}

View File

@ -0,0 +1,22 @@
import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
import { OrganizationUserType } from "jslib-common/enums/organizationUserType";
import { Organization } from "jslib-common/models/domain/organization";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class OrganizationResponse implements BaseResponse {
object: string;
id: string;
name: string;
status: OrganizationUserStatusType;
type: OrganizationUserType;
enabled: boolean;
constructor(o: Organization) {
this.object = "organization";
this.id = o.id;
this.name = o.name;
this.status = o.status;
this.type = o.type;
this.enabled = o.enabled;
}
}

View File

@ -0,0 +1,17 @@
import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
import { OrganizationUserType } from "jslib-common/enums/organizationUserType";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class OrganizationUserResponse implements BaseResponse {
object: string;
id: string;
email: string;
name: string;
status: OrganizationUserStatusType;
type: OrganizationUserType;
twoFactorEnabled: boolean;
constructor() {
this.object = "org-member";
}
}

View File

@ -0,0 +1,11 @@
import { PasswordHistoryView } from "jslib-common/models/view/passwordHistoryView";
export class PasswordHistoryResponse {
lastUsedDate: Date;
password: string;
constructor(o: PasswordHistoryView) {
this.lastUsedDate = o.lastUsedDate;
this.password = o.password;
}
}

View File

@ -0,0 +1,40 @@
import { SendType } from "jslib-common/enums/sendType";
import { SendAccessView } from "jslib-common/models/view/sendAccessView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { SendFileResponse } from "./sendFileResponse";
import { SendTextResponse } from "./sendTextResponse";
export class SendAccessResponse implements BaseResponse {
static template(): SendAccessResponse {
const req = new SendAccessResponse();
req.name = "Send name";
req.type = SendType.Text;
req.text = null;
req.file = null;
return req;
}
object = "send-access";
id: string;
name: string;
type: SendType;
text: SendTextResponse;
file: SendFileResponse;
constructor(o?: SendAccessView) {
if (o == null) {
return;
}
this.id = o.id;
this.name = o.name;
this.type = o.type;
if (o.type === SendType.Text && o.text != null) {
this.text = new SendTextResponse(o.text);
}
if (o.type === SendType.File && o.file != null) {
this.file = new SendFileResponse(o.file);
}
}
}

View File

@ -0,0 +1,36 @@
import { SendFileView } from "jslib-common/models/view/sendFileView";
export class SendFileResponse {
static template(fileName = "file attachment location"): SendFileResponse {
const req = new SendFileResponse();
req.fileName = fileName;
return req;
}
static toView(file: SendFileResponse, view = new SendFileView()) {
if (file == null) {
return null;
}
view.id = file.id;
view.size = file.size;
view.sizeName = file.sizeName;
view.fileName = file.fileName;
return view;
}
id: string;
size: string;
sizeName: string;
fileName: string;
constructor(o?: SendFileView) {
if (o == null) {
return;
}
this.id = o.id;
this.size = o.size;
this.sizeName = o.sizeName;
this.fileName = o.fileName;
}
}

View File

@ -0,0 +1,123 @@
import { SendType } from "jslib-common/enums/sendType";
import { Utils } from "jslib-common/misc/utils";
import { SendView } from "jslib-common/models/view/sendView";
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { SendFileResponse } from "./sendFileResponse";
import { SendTextResponse } from "./sendTextResponse";
const dateProperties: string[] = [
Utils.nameOf<SendResponse>("deletionDate"),
Utils.nameOf<SendResponse>("expirationDate"),
];
export class SendResponse implements BaseResponse {
static template(sendType?: SendType, deleteInDays = 7): SendResponse {
const req = new SendResponse();
req.name = "Send name";
req.notes = "Some notes about this send.";
req.type = sendType === SendType.File ? SendType.File : SendType.Text;
req.text = sendType === SendType.Text ? SendTextResponse.template() : null;
req.file = sendType === SendType.File ? SendFileResponse.template() : null;
req.maxAccessCount = null;
req.deletionDate = this.getStandardDeletionDate(deleteInDays);
req.expirationDate = null;
req.password = null;
req.disabled = false;
req.hideEmail = false;
return req;
}
static toView(send: SendResponse, view = new SendView()): SendView {
if (send == null) {
return null;
}
view.id = send.id;
view.accessId = send.accessId;
view.name = send.name;
view.notes = send.notes;
view.key = send.key == null ? null : Utils.fromB64ToArray(send.key);
view.type = send.type;
view.file = SendFileResponse.toView(send.file);
view.text = SendTextResponse.toView(send.text);
view.maxAccessCount = send.maxAccessCount;
view.accessCount = send.accessCount;
view.revisionDate = send.revisionDate;
view.deletionDate = send.deletionDate;
view.expirationDate = send.expirationDate;
view.password = send.password;
view.disabled = send.disabled;
view.hideEmail = send.hideEmail;
return view;
}
static fromJson(json: string) {
return JSON.parse(json, (key, value) => {
if (dateProperties.includes(key)) {
return value == null ? null : new Date(value);
}
return value;
});
}
private static getStandardDeletionDate(days: number) {
const d = new Date();
d.setTime(d.getTime() + days * 86400000); // ms per day
return d;
}
object = "send";
id: string;
accessId: string;
accessUrl: string;
name: string;
notes: string;
key: string;
type: SendType;
text: SendTextResponse;
file: SendFileResponse;
maxAccessCount?: number;
accessCount: number;
revisionDate: Date;
deletionDate: Date;
expirationDate: Date;
password: string;
passwordSet: boolean;
disabled: boolean;
hideEmail: boolean;
constructor(o?: SendView, webVaultUrl?: string) {
if (o == null) {
return;
}
this.id = o.id;
this.accessId = o.accessId;
let sendLinkBaseUrl = webVaultUrl;
if (sendLinkBaseUrl == null) {
sendLinkBaseUrl = "https://send.bitwarden.com/#";
} else {
sendLinkBaseUrl += "/#/send/";
}
this.accessUrl = sendLinkBaseUrl + this.accessId + "/" + o.urlB64Key;
this.name = o.name;
this.notes = o.notes;
this.key = Utils.fromBufferToB64(o.key);
this.type = o.type;
this.maxAccessCount = o.maxAccessCount;
this.accessCount = o.accessCount;
this.revisionDate = o.revisionDate;
this.deletionDate = o.deletionDate;
this.expirationDate = o.expirationDate;
this.passwordSet = o.password != null;
this.disabled = o.disabled;
this.hideEmail = o.hideEmail;
if (o.type === SendType.Text && o.text != null) {
this.text = new SendTextResponse(o.text);
}
if (o.type === SendType.File && o.file != null) {
this.file = new SendFileResponse(o.file);
}
}
}

View File

@ -0,0 +1,30 @@
import { SendTextView } from "jslib-common/models/view/sendTextView";
export class SendTextResponse {
static template(text = "Text contained in the send.", hidden = false): SendTextResponse {
const req = new SendTextResponse();
req.text = text;
req.hidden = hidden;
return req;
}
static toView(text: SendTextResponse, view = new SendTextView()) {
if (text == null) {
return null;
}
view.text = text.text;
view.hidden = text.hidden;
return view;
}
text: string;
hidden: boolean;
constructor(o?: SendTextView) {
if (o == null) {
return;
}
this.text = o.text;
this.hidden = o.hidden;
}
}

View File

@ -0,0 +1,11 @@
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
export class TemplateResponse implements BaseResponse {
object: string;
template: any;
constructor(template: any) {
this.object = "template";
this.template = template;
}
}

View File

@ -0,0 +1,15 @@
export class SelectionReadOnly {
static template(): SelectionReadOnly {
return new SelectionReadOnly("00000000-0000-0000-0000-000000000000", false, false);
}
id: string;
readOnly: boolean;
hidePasswords: boolean;
constructor(id: string, readOnly: boolean, hidePasswords: boolean) {
this.id = id;
this.readOnly = readOnly;
this.hidePasswords = hidePasswords || false;
}
}

546
apps/cli/src/program.ts Normal file
View File

@ -0,0 +1,546 @@
import * as chalk from "chalk";
import * as program from "commander";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions";
import { BaseProgram } from "jslib-node/cli/baseProgram";
import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
import { UpdateCommand } from "jslib-node/cli/commands/update.command";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { Main } from "./bw";
import { CompletionCommand } from "./commands/completion.command";
import { ConfigCommand } from "./commands/config.command";
import { EncodeCommand } from "./commands/encode.command";
import { GenerateCommand } from "./commands/generate.command";
import { LockCommand } from "./commands/lock.command";
import { LoginCommand } from "./commands/login.command";
import { ServeCommand } from "./commands/serve.command";
import { StatusCommand } from "./commands/status.command";
import { SyncCommand } from "./commands/sync.command";
import { UnlockCommand } from "./commands/unlock.command";
import { TemplateResponse } from "./models/response/templateResponse";
import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn;
export class Program extends BaseProgram {
constructor(protected main: Main) {
super(main.stateService, writeLn);
}
async register() {
program
.option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option("--raw", "Return raw output instead of a descriptive message.")
.option("--response", "Return a JSON formatted version of response output.")
.option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
.option("--quiet", "Don't return anything to stdout.")
.option("--nointeraction", "Do not prompt for interactive user input.")
.option("--session <session>", "Pass session key instead of reading from env.")
.version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
program.on("option:pretty", () => {
process.env.BW_PRETTY = "true";
});
program.on("option:raw", () => {
process.env.BW_RAW = "true";
});
program.on("option:quiet", () => {
process.env.BW_QUIET = "true";
});
program.on("option:response", () => {
process.env.BW_RESPONSE = "true";
});
program.on("option:cleanexit", () => {
process.env.BW_CLEANEXIT = "true";
});
program.on("option:nointeraction", () => {
process.env.BW_NOINTERACTION = "true";
});
program.on("option:session", (key) => {
process.env.BW_SESSION = key;
});
program.on("command:*", () => {
writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
writeLn("See --help for a list of available commands.", true, true);
process.exitCode = 1;
});
program.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw login");
writeLn(" bw lock");
writeLn(" bw unlock myPassword321");
writeLn(" bw list --help");
writeLn(" bw list items --search google");
writeLn(" bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(" bw get password google.com");
writeLn(' echo \'{"name":"My Folder"}\' | bw encode');
writeLn(" bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K");
writeLn(
" bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=="
);
writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(" bw generate -lusn --length 18");
writeLn(" bw config server https://bitwarden.example.com");
writeLn(" bw send -f ./file.ext");
writeLn(' bw send "text to send"');
writeLn(' echo "text to send" | bw send');
writeLn(
" bw receive https://vault.bitwarden.com/#/send/rg3iuoS_Akm2gqy6ADRHmg/Ht7dYjsqjmgqUM3rjzZDSQ"
);
writeLn("", true);
});
program
.command("login [email] [password]")
.description("Log into a user account.")
.option("--method <method>", "Two-step login method.")
.option("--code <code>", "Two-step login code.")
.option("--sso", "Log in with Single-Sign On.")
.option("--apikey", "Log in with an Api Key.")
.option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option(
"--passwordfile <passwordfile>",
"Path to a file containing your password as its first line"
)
.option("--check", "Check login status.", async () => {
const authed = await this.main.stateService.getIsAuthenticated();
if (authed) {
const res = new MessageResponse("You are logged in!", null);
this.processResponse(Response.success(res), true);
}
this.processResponse(Response.error("You are not logged in."), true);
})
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" See docs for valid `method` enum values.");
writeLn("");
writeLn(" Pass `--raw` option to only return the session key.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw login");
writeLn(" bw login john@example.com myPassword321 --raw");
writeLn(" bw login john@example.com myPassword321 --method 1 --code 249213");
writeLn(" bw login --sso");
writeLn("", true);
})
.action(async (email: string, password: string, options: program.OptionValues) => {
if (!options.check) {
await this.exitIfAuthed();
const command = new LoginCommand(
this.main.authService,
this.main.apiService,
this.main.cryptoFunctionService,
this.main.i18nService,
this.main.environmentService,
this.main.passwordGenerationService,
this.main.platformUtilsService,
this.main.stateService,
this.main.cryptoService,
this.main.policyService,
this.main.twoFactorService,
this.main.syncService,
this.main.keyConnectorService,
async () => await this.main.logout()
);
const response = await command.run(email, password, options);
this.processResponse(response);
}
});
program
.command("logout")
.description("Log out of the current user account.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw logout");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout()
);
const response = await command.run();
this.processResponse(response);
});
program
.command("lock")
.description("Lock the vault and destroy active session keys.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw lock");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
if (await this.main.keyConnectorService.getUsesKeyConnector()) {
const logoutCommand = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout()
);
await logoutCommand.run();
this.processResponse(
Response.error(
"You cannot lock your vault because you are using Key Connector. " +
"To protect your vault, you have been logged out."
),
true
);
return;
}
const command = new LockCommand(this.main.vaultTimeoutService);
const response = await command.run();
this.processResponse(response);
});
program
.command("unlock [password]")
.description("Unlock the vault and return a new session key.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" After unlocking, any previous session keys will no longer be valid.");
writeLn("");
writeLn(" Pass `--raw` option to only return the session key.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw unlock");
writeLn(" bw unlock myPassword321");
writeLn(" bw unlock myPassword321 --raw");
writeLn("", true);
})
.option("--check", "Check lock status.", async () => {
await this.exitIfNotAuthed();
const authStatus = await this.main.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Unlocked) {
const res = new MessageResponse("Vault is unlocked!", null);
this.processResponse(Response.success(res), true);
} else {
this.processResponse(Response.error("Vault is locked."), true);
}
})
.option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option(
"--passwordfile <passwordfile>",
"Path to a file containing your password as its first line"
)
.action(async (password, cmd) => {
if (!cmd.check) {
await this.exitIfNotAuthed();
const command = new UnlockCommand(
this.main.cryptoService,
this.main.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService,
this.main.keyConnectorService,
this.main.environmentService,
this.main.syncService,
async () => await this.main.logout()
);
const response = await command.run(password, cmd);
this.processResponse(response);
}
});
program
.command("sync")
.description("Pull the latest vault data from server.")
.option("-f, --force", "Force a full sync.")
.option("--last", "Get the last sync date.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw sync");
writeLn(" bw sync -f");
writeLn(" bw sync --last");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfLocked();
const command = new SyncCommand(this.main.syncService);
const response = await command.run(cmd);
this.processResponse(response);
});
program
.command("generate")
.description("Generate a password/passphrase.")
.option("-u, --uppercase", "Include uppercase characters.")
.option("-l, --lowercase", "Include lowercase characters.")
.option("-n, --number", "Include numeric characters.")
.option("-s, --special", "Include special characters.")
.option("-p, --passphrase", "Generate a passphrase.")
.option("--length <length>", "Length of the password.")
.option("--words <words>", "Number of words.")
.option("--separator <separator>", "Word separator.")
.option("-c, --capitalize", "Title case passphrase.")
.option("--includeNumber", "Passphrase includes number.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Default options are `-uln --length 14`.");
writeLn("");
writeLn(" Minimum `length` is 5.");
writeLn("");
writeLn(" Minimum `words` is 3.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw generate");
writeLn(" bw generate -u -l --length 18");
writeLn(" bw generate -ulns --length 25");
writeLn(" bw generate -ul");
writeLn(" bw generate -p --separator _");
writeLn(" bw generate -p --words 5 --separator space");
writeLn("", true);
})
.action(async (options) => {
const command = new GenerateCommand(
this.main.passwordGenerationService,
this.main.stateService
);
const response = await command.run(options);
this.processResponse(response);
});
program
.command("encode")
.description("Base 64 encode stdin.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Use to create `encodedJson` for `create` and `edit` commands.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(' echo \'{"name":"My Folder"}\' | bw encode');
writeLn("", true);
})
.action(async () => {
const command = new EncodeCommand();
const response = await command.run();
this.processResponse(response);
});
program
.command("config <setting> [value]")
.description("Configure CLI settings.")
.option(
"--web-vault <url>",
"Provides a custom web vault URL that differs from the base URL."
)
.option("--api <url>", "Provides a custom API URL that differs from the base URL.")
.option("--identity <url>", "Provides a custom identity URL that differs from the base URL.")
.option(
"--icons <url>",
"Provides a custom icons service URL that differs from the base URL."
)
.option(
"--notifications <url>",
"Provides a custom notifications URL that differs from the base URL."
)
.option("--events <url>", "Provides a custom events URL that differs from the base URL.")
.option("--key-connector <url>", "Provides the URL for your Key Connector server.")
.on("--help", () => {
writeLn("\n Settings:");
writeLn("");
writeLn(" server - On-premises hosted installation URL.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw config server");
writeLn(" bw config server https://bw.company.com");
writeLn(" bw config server bitwarden.com");
writeLn(
" bw config server --api http://localhost:4000 --identity http://localhost:33656"
);
writeLn("", true);
})
.action(async (setting, value, options) => {
const command = new ConfigCommand(this.main.environmentService);
const response = await command.run(setting, value, options);
this.processResponse(response);
});
program
.command("update")
.description("Check for updates.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Returns the URL to download the newest version of this CLI tool.");
writeLn("");
writeLn(" Use the `--raw` option to return only the download URL for the update.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw update");
writeLn(" bw update --raw");
writeLn("", true);
})
.action(async () => {
const command = new UpdateCommand(
this.main.platformUtilsService,
this.main.i18nService,
"cli",
"bw",
true
);
const response = await command.run();
this.processResponse(response);
});
program
.command("completion")
.description("Generate shell completions.")
.option("--shell <shell>", "Shell to generate completions for.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Valid shells are `zsh`.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw completion --shell zsh");
writeLn("", true);
})
.action(async (options: program.OptionValues, cmd: program.Command) => {
const command = new CompletionCommand();
const response = await command.run(options);
this.processResponse(response);
});
program
.command("status")
.description("Show server, last sync, user information, and vault status.")
.on("--help", () => {
writeLn("");
writeLn("");
writeLn(" Example return value:");
writeLn("");
writeLn(" {");
writeLn(' "serverUrl": "https://bitwarden.example.com",');
writeLn(' "lastSync": "2020-06-16T06:33:51.419Z",');
writeLn(' "userEmail": "user@example.com,');
writeLn(' "userId": "00000000-0000-0000-0000-000000000000",');
writeLn(' "status": "locked"');
writeLn(" }");
writeLn("");
writeLn(" Notes:");
writeLn("");
writeLn(" `status` is one of:");
writeLn(" - `unauthenticated` when you are not logged in");
writeLn(" - `locked` when you are logged in and the vault is locked");
writeLn(" - `unlocked` when you are logged in and the vault is unlocked");
writeLn("", true);
})
.action(async () => {
const command = new StatusCommand(
this.main.environmentService,
this.main.syncService,
this.main.stateService,
this.main.authService
);
const response = await command.run();
this.processResponse(response);
});
if (CliUtils.flagEnabled("serve")) {
program
.command("serve")
.description("Start a RESTful API webserver.")
.option("--hostname <hostname>", "The hostname to bind your API webserver to.")
.option("--port <port>", "The port to run your API webserver on.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Default hostname is `localhost`.");
writeLn(" Use hostname `all` for no hostname binding.");
writeLn(" Default port is `8087`.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw serve");
writeLn(" bw serve --port 8080");
writeLn(" bw serve --hostname bwapi.mydomain.com --port 80");
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const command = new ServeCommand(this.main);
await command.run(cmd);
});
}
}
protected processResponse(response: Response, exitImmediately = false) {
super.processResponse(response, exitImmediately, () => {
if (response.data.object === "template") {
return this.getJson((response.data as TemplateResponse).template);
}
return null;
});
}
protected async exitIfLocked() {
await this.exitIfNotAuthed();
if (await this.main.cryptoService.hasKeyInMemory()) {
return;
} else if (await this.main.cryptoService.hasKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
await this.main.cryptoService.getKey();
} else if (process.env.BW_NOINTERACTION !== "true") {
// must unlock
if (await this.main.keyConnectorService.getUsesKeyConnector()) {
const response = Response.error(
"Your vault is locked. You must unlock your vault using your session key.\n" +
"If you do not have your session key, you can get a new one by logging out and logging in again."
);
this.processResponse(response, true);
} else {
const command = new UnlockCommand(
this.main.cryptoService,
this.main.stateService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService,
this.main.keyConnectorService,
this.main.environmentService,
this.main.syncService,
this.main.logout
);
const response = await command.run(null, null);
if (!response.success) {
this.processResponse(response, true);
}
}
} else {
this.processResponse(Response.error("Vault is locked."), true);
}
}
}

View File

@ -0,0 +1,334 @@
import * as fs from "fs";
import * as path from "path";
import * as chalk from "chalk";
import * as program from "commander";
import { SendType } from "jslib-common/enums/sendType";
import { Utils } from "jslib-common/misc/utils";
import { Response } from "jslib-node/cli/models/response";
import { Main } from "./bw";
import { GetCommand } from "./commands/get.command";
import { SendCreateCommand } from "./commands/send/create.command";
import { SendDeleteCommand } from "./commands/send/delete.command";
import { SendEditCommand } from "./commands/send/edit.command";
import { SendGetCommand } from "./commands/send/get.command";
import { SendListCommand } from "./commands/send/list.command";
import { SendReceiveCommand } from "./commands/send/receive.command";
import { SendRemovePasswordCommand } from "./commands/send/removePassword.command";
import { SendFileResponse } from "./models/response/sendFileResponse";
import { SendResponse } from "./models/response/sendResponse";
import { SendTextResponse } from "./models/response/sendTextResponse";
import { Program } from "./program";
import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn;
export class SendProgram extends Program {
constructor(main: Main) {
super(main);
}
async register() {
program.addCommand(this.sendCommand());
// receive is accessible both at `bw receive` and `bw send receive`
program.addCommand(this.receiveCommand());
}
private sendCommand(): program.Command {
return new program.Command("send")
.arguments("<data>")
.description(
"Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send",
{
data: "The data to Send. Specify as a filepath with the --file option",
}
)
.option("-f, --file", "Specifies that <data> is a filepath")
.option(
"-d, --deleteInDays <days>",
"The number of days in the future to set deletion date, defaults to 7",
"7"
)
.option("-a, --maxAccessCount <amount>", "The amount of max possible accesses.")
.option("--hidden", "Hide <data> in web by default. Valid only if --file is not set.")
.option(
"-n, --name <name>",
"The name of the Send. Defaults to a guid for text Sends and the filename for files."
)
.option("--notes <notes>", "Notes to add to the Send.")
.option(
"--fullObject",
"Specifies that the full Send object should be returned rather than just the access url."
)
.addCommand(this.listCommand())
.addCommand(this.templateCommand())
.addCommand(this.getCommand())
.addCommand(this.receiveCommand())
.addCommand(this.createCommand())
.addCommand(this.editCommand())
.addCommand(this.removePasswordCommand())
.addCommand(this.deleteCommand())
.action(async (data: string, options: program.OptionValues) => {
const encodedJson = this.makeSendJson(data, options);
let response: Response;
if (encodedJson instanceof Response) {
response = encodedJson;
} else {
response = await this.runCreate(encodedJson, options);
}
this.processResponse(response);
});
}
private receiveCommand(): program.Command {
return new program.Command("receive")
.arguments("<url>")
.description("Access a Bitwarden Send from a url")
.option("--password <password>", "Password needed to access the Send.")
.option("--passwordenv <passwordenv>", "Environment variable storing the Send's password")
.option(
"--passwordfile <passwordfile>",
"Path to a file containing the Sends password as its first line"
)
.option("--obj", "Return the Send's json object rather than the Send's content")
.option("--output <location>", "Specify a file path to save a File-type Send to")
.on("--help", () => {
writeLn("");
writeLn(
"If a password is required, the provided password is used or the user is prompted."
);
writeLn("", true);
})
.action(async (url: string, options: program.OptionValues) => {
const cmd = new SendReceiveCommand(
this.main.apiService,
this.main.cryptoService,
this.main.cryptoFunctionService,
this.main.platformUtilsService,
this.main.environmentService
);
const response = await cmd.run(url, options);
this.processResponse(response);
});
}
private listCommand(): program.Command {
return new program.Command("list")
.description("List all the Sends owned by you")
.on("--help", () => {
writeLn(chalk("This is in the list command"));
})
.action(async (options: program.OptionValues) => {
await this.exitIfLocked();
const cmd = new SendListCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService
);
const response = await cmd.run(options);
this.processResponse(response);
});
}
private templateCommand(): program.Command {
return new program.Command("template")
.arguments("<object>")
.description("Get json templates for send objects", {
object: "Valid objects are: send, send.text, send.file",
})
.action(async (object) => {
const cmd = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
const response = await cmd.run("template", object, null);
this.processResponse(response);
});
}
private getCommand(): program.Command {
return new program.Command("get")
.arguments("<id>")
.description("Get Sends owned by you.")
.option("--output <output>", "Output directory or filename for attachment.")
.option("--text", "Specifies to return the text content of a Send")
.on("--help", () => {
writeLn("");
writeLn(" Id:");
writeLn("");
writeLn(" Search term or Send's globally unique `id`.");
writeLn("");
writeLn(" If raw output is specified and no output filename or directory is given for");
writeLn(" an attachment query, the attachment content is written to stdout.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw get send searchText");
writeLn(" bw get send id");
writeLn(" bw get send searchText --text");
writeLn(" bw get send searchText --file");
writeLn(" bw get send searchText --file --output ../Photos/photo.jpg");
writeLn(" bw get send searchText --file --raw");
writeLn("", true);
})
.action(async (id: string, options: program.OptionValues) => {
await this.exitIfLocked();
const cmd = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
const response = await cmd.run(id, options);
this.processResponse(response);
});
}
private createCommand(): program.Command {
return new program.Command("create")
.arguments("[encodedJson]")
.description("create a Send", {
encodedJson: "JSON object to upload. Can also be piped in through stdin.",
})
.option("--file <path>", "file to Send. Can also be specified in parent's JSON.")
.option("--text <text>", "text to Send. Can also be specified in parent's JSON.")
.option("--hidden", "text hidden flag. Valid only with the --text option.")
.option(
"--password <password>",
"optional password to access this Send. Can also be specified in JSON"
)
.on("--help", () => {
writeLn("");
writeLn("Note:");
writeLn(" Options specified in JSON take precedence over command options");
writeLn("", true);
})
.action(
async (
encodedJson: string,
options: program.OptionValues,
args: { parent: program.Command }
) => {
// Work-around to support `--fullObject` option for `send create --fullObject`
// Calling `option('--fullObject', ...)` above won't work due to Commander doesn't like same option
// to be defind on both parent-command and sub-command
const { fullObject = false } = args.parent.opts();
const mergedOptions = {
...options,
fullObject: fullObject,
};
const response = await this.runCreate(encodedJson, mergedOptions);
this.processResponse(response);
}
);
}
private editCommand(): program.Command {
return new program.Command("edit")
.arguments("[encodedJson]")
.description("edit a Send", {
encodedJson:
"Updated JSON object to save. If not provided, encodedJson is read from stdin.",
})
.option("--itemid <itemid>", "Overrides the itemId provided in [encodedJson]")
.on("--help", () => {
writeLn("");
writeLn("Note:");
writeLn(" You cannot update a File-type Send's file. Just delete and remake it");
writeLn("", true);
})
.action(async (encodedJson: string, options: program.OptionValues) => {
await this.exitIfLocked();
const getCmd = new SendGetCommand(
this.main.sendService,
this.main.environmentService,
this.main.searchService,
this.main.cryptoService
);
const cmd = new SendEditCommand(this.main.sendService, this.main.stateService, getCmd);
const response = await cmd.run(encodedJson, options);
this.processResponse(response);
});
}
private deleteCommand(): program.Command {
return new program.Command("delete")
.arguments("<id>")
.description("delete a Send", {
id: "The id of the Send to delete.",
})
.action(async (id: string) => {
await this.exitIfLocked();
const cmd = new SendDeleteCommand(this.main.sendService);
const response = await cmd.run(id);
this.processResponse(response);
});
}
private removePasswordCommand(): program.Command {
return new program.Command("remove-password")
.arguments("<id>")
.description("removes the saved password from a Send.", {
id: "The id of the Send to alter.",
})
.action(async (id: string) => {
await this.exitIfLocked();
const cmd = new SendRemovePasswordCommand(this.main.sendService);
const response = await cmd.run(id);
this.processResponse(response);
});
}
private makeSendJson(data: string, options: program.OptionValues) {
let sendFile = null;
let sendText = null;
let name = Utils.newGuid();
let type = SendType.Text;
if (options.file != null) {
data = path.resolve(data);
if (!fs.existsSync(data)) {
return Response.badRequest("data path does not exist");
}
sendFile = SendFileResponse.template(data);
name = path.basename(data);
type = SendType.File;
} else {
sendText = SendTextResponse.template(data, options.hidden);
}
const template = Utils.assign(SendResponse.template(null, options.deleteInDays), {
name: options.name ?? name,
notes: options.notes,
file: sendFile,
text: sendText,
type: type,
});
return Buffer.from(JSON.stringify(template), "utf8").toString("base64");
}
private async runCreate(encodedJson: string, options: program.OptionValues) {
await this.exitIfLocked();
const cmd = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService
);
return await cmd.run(encodedJson, options);
}
}

View File

@ -0,0 +1,20 @@
import * as fs from "fs";
import * as path from "path";
import { I18nService as BaseI18nService } from "jslib-common/services/i18n.service";
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
const filePath = path.join(
__dirname,
this.localesDirectory + "/" + formattedLocale + "/messages.json"
);
const localesJson = fs.readFileSync(filePath, "utf8");
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
});
this.supportedTranslationLocales = ["en"];
}
}

View File

@ -0,0 +1,40 @@
import * as lock from "proper-lockfile";
import { OperationOptions } from "retry";
import { LogService } from "jslib-common/abstractions/log.service";
import { Utils } from "jslib-common/misc/utils";
import { LowdbStorageService as LowdbStorageServiceBase } from "jslib-node/services/lowdbStorage.service";
const retries: OperationOptions = {
retries: 50,
minTimeout: 100,
maxTimeout: 250,
factor: 2,
};
export class LowdbStorageService extends LowdbStorageServiceBase {
constructor(
logService: LogService,
defaults?: any,
dir?: string,
allowCache = false,
private requireLock = false
) {
super(logService, defaults, dir, allowCache);
}
protected async lockDbFile<T>(action: () => T): Promise<T> {
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
this.logService.info("acquiring db file lock");
return await lock.lock(this.dataFilePath, { retries: retries }).then((release) => {
try {
return action();
} finally {
release();
}
});
} else {
return action();
}
}
}

View File

@ -0,0 +1,103 @@
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { StorageService } from "jslib-common/abstractions/storage.service";
import { Utils } from "jslib-common/misc/utils";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
export class NodeEnvSecureStorageService implements StorageService {
constructor(
private storageService: StorageService,
private logService: LogService,
private cryptoService: () => CryptoService
) {}
async get<T>(key: string): Promise<T> {
const value = await this.storageService.get<string>(this.makeProtectedStorageKey(key));
if (value == null) {
return null;
}
const obj = await this.decrypt(value);
return obj as any;
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
async save(key: string, obj: any): Promise<any> {
if (obj == null) {
return this.remove(key);
}
if (obj !== null && typeof obj !== "string") {
throw new Error("Only string storage is allowed.");
}
const protectedObj = await this.encrypt(obj);
await this.storageService.save(this.makeProtectedStorageKey(key), protectedObj);
}
remove(key: string): Promise<any> {
return this.storageService.remove(this.makeProtectedStorageKey(key));
}
private async encrypt(plainValue: string): Promise<string> {
const sessionKey = this.getSessionKey();
if (sessionKey == null) {
throw new Error("No session key available.");
}
const encValue = await this.cryptoService().encryptToBytes(
Utils.fromB64ToArray(plainValue).buffer,
sessionKey
);
if (encValue == null) {
throw new Error("Value didn't encrypt.");
}
return Utils.fromBufferToB64(encValue.buffer);
}
private async decrypt(encValue: string): Promise<string> {
try {
const sessionKey = this.getSessionKey();
if (sessionKey == null) {
return null;
}
const decValue = await this.cryptoService().decryptFromBytes(
Utils.fromB64ToArray(encValue).buffer,
sessionKey
);
if (decValue == null) {
this.logService.info("Failed to decrypt.");
return null;
}
return Utils.fromBufferToB64(decValue);
} catch (e) {
this.logService.info("Decrypt error.");
return null;
}
}
private getSessionKey() {
try {
if (process.env.BW_SESSION != null) {
const sessionBuffer = Utils.fromB64ToArray(process.env.BW_SESSION).buffer;
if (sessionBuffer != null) {
const sessionKey = new SymmetricCryptoKey(sessionBuffer);
if (sessionBuffer != null) {
return sessionKey;
}
}
}
} catch (e) {
this.logService.info("Session key is invalid.");
}
return null;
}
private makeProtectedStorageKey(key: string) {
return "__PROTECTED__" + key;
}
}

270
apps/cli/src/utils.ts Normal file
View File

@ -0,0 +1,270 @@
import * as fs from "fs";
import * as path from "path";
import * as inquirer from "inquirer";
import * as JSZip from "jszip";
import { LogService } from "jslib-common/abstractions/log.service";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Utils } from "jslib-common/misc/utils";
import { Organization } from "jslib-common/models/domain/organization";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { FlagName, Flags } from "./flags";
export class CliUtils {
static writeLn(s: string, finalLine = false, error = false) {
const stream = error ? process.stderr : process.stdout;
if (finalLine && (process.platform === "win32" || !stream.isTTY)) {
stream.write(s);
} else {
stream.write(s + "\n");
}
}
static readFile(input: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
let p: string = null;
if (input != null && input !== "") {
const osInput = path.join(input);
if (osInput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osInput);
} else {
p = osInput;
}
} else {
reject("You must specify a file path.");
}
fs.readFile(p, "utf8", (err, data) => {
if (err != null) {
reject(err.message);
}
resolve(data);
});
});
}
static extract1PuxContent(input: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
let p: string = null;
if (input != null && input !== "") {
const osInput = path.join(input);
if (osInput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osInput);
} else {
p = osInput;
}
} else {
reject("You must specify a file path.");
}
fs.readFile(p, function (err, data) {
if (err) {
reject(err);
}
JSZip.loadAsync(data).then(
(zip) => {
resolve(zip.file("export.data").async("string"));
},
(reason) => {
reject(reason);
}
);
});
});
}
/**
* Save the given data to a file and determine the target file if necessary.
* If output is non-empty, it is used as target filename. Otherwise the target filename is
* built from the current working directory and the given defaultFileName.
*
* @param data to be written to the file.
* @param output file to write to or empty to choose automatically.
* @param defaultFileName to use when no explicit output filename is given.
* @return the chosen output file.
*/
static saveFile(data: string | Buffer, output: string, defaultFileName: string) {
let p: string = null;
let mkdir = false;
if (output != null && output !== "") {
const osOutput = path.join(output);
if (osOutput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osOutput);
} else {
mkdir = true;
if (osOutput.endsWith(path.sep)) {
p = path.join(osOutput, defaultFileName);
} else {
p = osOutput;
}
}
} else {
p = path.join(process.cwd(), defaultFileName);
}
p = path.resolve(p);
if (mkdir) {
const dir = p.substring(0, p.lastIndexOf(path.sep));
if (!fs.existsSync(dir)) {
NodeUtils.mkdirpSync(dir, "700");
}
}
return new Promise<string>((resolve, reject) => {
fs.writeFile(p, data, { encoding: "utf8", mode: 0o600 }, (err) => {
if (err != null) {
reject("Cannot save file to " + p);
}
resolve(p);
});
});
}
/**
* Process the given data and write it to a file if possible. If the user requested RAW output and
* no output name is given, the file is directly written to stdout. The resulting Response contains
* an otherwise empty message then to prevent writing other information to stdout.
*
* If an output is given or no RAW output is requested, the rules from [saveFile] apply.
*
* @param data to be written to the file or stdout.
* @param output file to write to or empty to choose automatically.
* @param defaultFileName to use when no explicit output filename is given.
* @return an empty [Response] if written to stdout or a [Response] with the chosen output file otherwise.
*/
static async saveResultToFile(data: string | Buffer, output: string, defaultFileName: string) {
if ((output == null || output === "") && process.env.BW_RAW === "true") {
// No output is given and the user expects raw output. Since the command result is about content,
// we directly return the command result to stdout (and suppress further messages).
process.stdout.write(data);
return Response.success();
}
const filePath = await this.saveFile(data, output, defaultFileName);
const res = new MessageResponse("Saved " + filePath, null);
res.raw = filePath;
return Response.success(res);
}
static readStdin(): Promise<string> {
return new Promise((resolve, reject) => {
let input = "";
if (process.stdin.isTTY) {
resolve(input);
return;
}
process.stdin.setEncoding("utf8");
process.stdin.on("readable", () => {
// eslint-disable-next-line
while (true) {
const chunk = process.stdin.read();
if (chunk == null) {
break;
}
input += chunk;
}
});
process.stdin.on("end", () => {
resolve(input);
});
});
}
static searchFolders(folders: FolderView[], search: string) {
search = search.toLowerCase();
return folders.filter((f) => {
if (f.name != null && f.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
static searchCollections(collections: CollectionView[], search: string) {
search = search.toLowerCase();
return collections.filter((c) => {
if (c.name != null && c.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
static searchOrganizations(organizations: Organization[], search: string) {
search = search.toLowerCase();
return organizations.filter((o) => {
if (o.name != null && o.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
/**
* Gets a password from all available sources. In order of priority these are:
* * passwordfile
* * passwordenv
* * user interaction
*
* Returns password string if successful, Response if not.
*/
static async getPassword(
password: string,
options: { passwordFile?: string; passwordEnv?: string },
logService?: LogService
): Promise<string | Response> {
if (Utils.isNullOrEmpty(password)) {
if (options?.passwordFile) {
password = await NodeUtils.readFirstLine(options.passwordFile);
} else if (options?.passwordEnv) {
if (process.env[options.passwordEnv]) {
password = process.env[options.passwordEnv];
} else if (logService) {
logService.warning(`Warning: Provided passwordenv ${options.passwordEnv} is not set`);
}
}
}
if (Utils.isNullOrEmpty(password)) {
if (process.env.BW_NOINTERACTION !== "true") {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Master password:",
});
password = answer.password;
} else {
return Response.badRequest(
"Master password is required. Try again in interactive mode or provide a password file or environment variable."
);
}
}
return password;
}
static convertBooleanOption(optionValue: any) {
return optionValue || optionValue === "" ? true : false;
}
static flagEnabled(flag: FlagName) {
return this.flags[flag] == null || this.flags[flag];
}
private static get flags(): Flags {
const envFlags = process.env.FLAGS;
if (typeof envFlags === "string") {
return JSON.parse(envFlags) as Flags;
} else {
return envFlags as Flags;
}
}
}

View File

@ -0,0 +1,486 @@
import * as program from "commander";
import { Response } from "jslib-node/cli/models/response";
import { Main } from "./bw";
import { ConfirmCommand } from "./commands/confirm.command";
import { CreateCommand } from "./commands/create.command";
import { DeleteCommand } from "./commands/delete.command";
import { EditCommand } from "./commands/edit.command";
import { ExportCommand } from "./commands/export.command";
import { GetCommand } from "./commands/get.command";
import { ImportCommand } from "./commands/import.command";
import { ListCommand } from "./commands/list.command";
import { RestoreCommand } from "./commands/restore.command";
import { ShareCommand } from "./commands/share.command";
import { Program } from "./program";
import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn;
export class VaultProgram extends Program {
constructor(protected main: Main) {
super(main);
}
async register() {
program
.addCommand(this.listCommand())
.addCommand(this.getCommand())
.addCommand(this.createCommand())
.addCommand(this.editCommand())
.addCommand(this.deleteCommand())
.addCommand(this.restoreCommand())
.addCommand(this.shareCommand("move", false))
.addCommand(this.confirmCommand())
.addCommand(this.importCommand())
.addCommand(this.exportCommand())
.addCommand(this.shareCommand("share", true));
}
private validateObject(requestedObject: string, validObjects: string[]): boolean {
let success = true;
if (!validObjects.includes(requestedObject)) {
success = false;
this.processResponse(
Response.badRequest(
'Unknown object "' +
requestedObject +
'". Allowed objects are ' +
validObjects.join(", ") +
"."
)
);
}
return success;
}
private listCommand(): program.Command {
const listObjects = [
"items",
"folders",
"collections",
"org-collections",
"org-members",
"organizations",
];
return new program.Command("list")
.arguments("<object>")
.description("List an array of objects from the vault.", {
object: "Valid objects are: " + listObjects.join(", "),
})
.option("--search <search>", "Perform a search on the listed objects.")
.option("--url <url>", "Filter items of type login with a url-match search.")
.option("--folderid <folderid>", "Filter items by folder id.")
.option("--collectionid <collectionid>", "Filter items by collection id.")
.option(
"--organizationid <organizationid>",
"Filter items or collections by organization id."
)
.option("--trash", "Filter items that are deleted and in the trash.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Combining search with a filter performs a logical AND operation.");
writeLn("");
writeLn(" Combining multiple filters performs a logical OR operation.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw list items");
writeLn(" bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2");
writeLn(
" bw list items --search google --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2"
);
writeLn(" bw list items --url https://google.com");
writeLn(" bw list items --folderid null");
writeLn(" bw list items --organizationid notnull");
writeLn(
" bw list items --folderid 60556c31-e649-4b5d-8daf-fc1c391a1bf2 --organizationid notnull"
);
writeLn(" bw list items --trash");
writeLn(" bw list folders --search email");
writeLn(" bw list org-members --organizationid 60556c31-e649-4b5d-8daf-fc1c391a1bf2");
writeLn("", true);
})
.action(async (object, cmd) => {
if (!this.validateObject(object, listObjects)) {
return;
}
await this.exitIfLocked();
const command = new ListCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.organizationService,
this.main.searchService,
this.main.apiService
);
const response = await command.run(object, cmd);
this.processResponse(response);
});
}
private getCommand(): program.Command {
const getObjects = [
"item",
"username",
"password",
"uri",
"totp",
"notes",
"exposed",
"attachment",
"folder",
"collection",
"org-collection",
"organization",
"template",
"fingerprint",
"send",
];
return new program.Command("get")
.arguments("<object> <id>")
.description("Get an object from the vault.", {
object: "Valid objects are: " + getObjects.join(", "),
id: "Search term or object's globally unique `id`.",
})
.option("--itemid <itemid>", "Attachment's item id.")
.option("--output <output>", "Output directory or filename for attachment.")
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n If raw output is specified and no output filename or directory is given for");
writeLn(" an attachment query, the attachment content is written to stdout.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(" bw get password https://google.com");
writeLn(" bw get totp google.com");
writeLn(" bw get notes google.com");
writeLn(" bw get exposed yahoo.com");
writeLn(
" bw get attachment b857igwl1dzrs2 --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 " +
"--output ./photo.jpg"
);
writeLn(
" bw get attachment photo.jpg --itemid 99ee88d2-6046-4ea7-92c2-acac464b1412 --raw"
);
writeLn(" bw get folder email");
writeLn(" bw get template folder");
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, getObjects)) {
return;
}
await this.exitIfLocked();
const command = new GetCommand(
this.main.cipherService,
this.main.folderService,
this.main.collectionService,
this.main.totpService,
this.main.auditService,
this.main.cryptoService,
this.main.stateService,
this.main.searchService,
this.main.apiService,
this.main.organizationService
);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private createCommand() {
const createObjects = ["item", "attachment", "folder", "org-collection"];
return new program.Command("create")
.arguments("<object> [encodedJson]")
.description("Create an object in the vault.", {
object: "Valid objects are: " + createObjects.join(", "),
encodedJson: "Encoded json of the object to create. Can also be piped into stdin.",
})
.option("--file <file>", "Path to file for attachment.")
.option("--itemid <itemid>", "Attachment's item id.")
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K");
writeLn(" echo 'eyJuYW1lIjoiTXkgRm9sZGVyIn0K' | bw create folder");
writeLn(
" bw create attachment --file ./myfile.csv " +
"--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66"
);
writeLn("", true);
})
.action(async (object, encodedJson, cmd) => {
if (!this.validateObject(object, createObjects)) {
return;
}
await this.exitIfLocked();
const command = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService
);
const response = await command.run(object, encodedJson, cmd);
this.processResponse(response);
});
}
private editCommand(): program.Command {
const editObjects = ["item", "item-collections", "folder", "org-collection"];
return new program.Command("edit")
.arguments("<object> <id> [encodedJson]")
.description("Edit an object from the vault.", {
object: "Valid objects are: " + editObjects.join(", "),
id: "Object's globally unique `id`.",
encodedJson: "Encoded json of the object to create. Can also be piped into stdin.",
})
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(
" bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=="
);
writeLn(
" echo 'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==' | " +
"bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02"
);
writeLn(
" bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 " +
"WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=="
);
writeLn("", true);
})
.action(async (object, id, encodedJson, cmd) => {
if (!this.validateObject(object, editObjects)) {
return;
}
await this.exitIfLocked();
const command = new EditCommand(
this.main.cipherService,
this.main.folderService,
this.main.cryptoService,
this.main.apiService
);
const response = await command.run(object, id, encodedJson, cmd);
this.processResponse(response);
});
}
private deleteCommand(): program.Command {
const deleteObjects = ["item", "attachment", "folder", "org-collection"];
return new program.Command("delete")
.arguments("<object> <id>")
.description("Delete an object from the vault.", {
object: "Valid objects are: " + deleteObjects.join(", "),
id: "Object's globally unique `id`.",
})
.option("--itemid <itemid>", "Attachment's item id.")
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.option(
"-p, --permanent",
"Permanently deletes the item instead of soft-deleting it (item only)."
)
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw delete item 7063feab-4b10-472e-b64c-785e2b870b92");
writeLn(" bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent");
writeLn(" bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02");
writeLn(
" bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78"
);
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, deleteObjects)) {
return;
}
await this.exitIfLocked();
const command = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService
);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private restoreCommand(): program.Command {
const restoreObjects = ["item"];
return new program.Command("restore")
.arguments("<object> <id>")
.description("Restores an object from the trash.", {
object: "Valid objects are: " + restoreObjects.join(", "),
id: "Object's globally unique `id`.",
})
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw restore item 7063feab-4b10-472e-b64c-785e2b870b92");
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, restoreObjects)) {
return;
}
await this.exitIfLocked();
const command = new RestoreCommand(this.main.cipherService);
const response = await command.run(object, id);
this.processResponse(response);
});
}
private shareCommand(commandName: string, deprecated: boolean): program.Command {
return new program.Command(commandName)
.arguments("<id> <organizationId> [encodedJson]")
.description((deprecated ? "--DEPRECATED-- " : "") + "Move an item to an organization.", {
id: "Object's globally unique `id`.",
organizationId: "Organization's globally unique `id`.",
encodedJson: "Encoded json of an array of collection ids. Can also be piped into stdin.",
})
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(
" bw " +
commandName +
" 4af958ce-96a7-45d9-beed-1e70fabaa27a " +
"6d82949b-b44d-468a-adae-3f3bacb0ea32 WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=="
);
writeLn(
" echo '[\"974053d0-3b33-4b98-886e-fecf5c8dba96\"]' | bw encode | " +
"bw " +
commandName +
" 4af958ce-96a7-45d9-beed-1e70fabaa27a 6d82949b-b44d-468a-adae-3f3bacb0ea32"
);
if (deprecated) {
writeLn("");
writeLn('--DEPRECATED See "bw move" for the current implementation--');
}
writeLn("", true);
})
.action(async (id, organizationId, encodedJson, cmd) => {
await this.exitIfLocked();
const command = new ShareCommand(this.main.cipherService);
const response = await command.run(id, organizationId, encodedJson);
this.processResponse(response);
});
}
private confirmCommand(): program.Command {
const confirmObjects = ["org-member"];
return new program.Command("confirm")
.arguments("<object> <id>")
.description("Confirm an object to the organization.", {
object: "Valid objects are: " + confirmObjects.join(", "),
id: "Object's globally unique `id`.",
})
.option("--organizationid <organizationid>", "Organization id for an organization object.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(
" bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 " +
"--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78"
);
writeLn("", true);
})
.action(async (object, id, cmd) => {
if (!this.validateObject(object, confirmObjects)) {
return;
}
await this.exitIfLocked();
const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private importCommand(): program.Command {
return new program.Command("import")
.arguments("[format] [input]")
.description("Import vault data from a file.", {
format: "The format of [input]",
input: "Filepath to data to import",
})
.option("--formats", "List formats")
.option("--organizationid <organizationid>", "ID of the organization to import to.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bw import --formats");
writeLn(" bw import bitwardencsv ./from/source.csv");
writeLn(" bw import keepass2xml keepass_backup.xml");
writeLn(
" bw import --organizationid cf14adc3-aca5-4573-890a-f6fa231436d9 keepass2xml keepass_backup.xml"
);
})
.action(async (format, filepath, options) => {
await this.exitIfLocked();
const command = new ImportCommand(this.main.importService, this.main.organizationService);
const response = await command.run(format, filepath, options);
this.processResponse(response);
});
}
private exportCommand(): program.Command {
return new program.Command("export")
.description("Export vault data to a CSV or JSON file.", {})
.option("--output <output>", "Output directory or filename.")
.option("--format <format>", "Export file format.")
.option(
"--password [password]",
"Use password to encrypt instead of your Bitwarden account encryption key. Only applies to the encrypted_json format."
)
.option("--organizationid <organizationid>", "Organization id for an organization.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(
" Valid formats are `csv`, `json`, and `encrypted_json`. Default format is `csv`."
);
writeLn("");
writeLn(
" If --raw option is specified and no output filename or directory is given, the"
);
writeLn(" result is written to stdout.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bw export");
writeLn(" bw --raw export");
writeLn(" bw export myPassword321");
writeLn(" bw export myPassword321 --format json");
writeLn(" bw export --output ./exp/bw.csv");
writeLn(" bw export myPassword321 --output bw.json --format json");
writeLn(
" bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92"
);
writeLn("", true);
})
.action(async (options) => {
await this.exitIfLocked();
const command = new ExportCommand(this.main.exportService, this.main.policyService);
const response = await command.run(options);
this.processResponse(response);
});
}
}

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not remove this test for UTF-8: if “Ω” doesnt appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>bitwarden-cli</id>
<version>0.0.0</version>
<packageSourceUrl>https://github.com/bitwarden/cli/tree/master/stores/chocolatey</packageSourceUrl>
<owners>kspearrin</owners>
<title>Bitwarden CLI</title>
<authors>Bitwarden Inc.</authors>
<projectUrl>https://bitwarden.com/</projectUrl>
<iconUrl>https://cdn.rawgit.com/bitwarden/desktop/51dd1341/resources/icon.png</iconUrl>
<copyright>Copyright © 2015-2022 Bitwarden Inc.</copyright>
<projectSourceUrl>https://github.com/bitwarden/cli/</projectSourceUrl>
<docsUrl>https://help.bitwarden.com/article/cli/</docsUrl>
<bugTrackerUrl>https://github.com/bitwarden/cli/issues</bugTrackerUrl>
<releaseNotes>https://github.com/bitwarden/cli/releases</releaseNotes>
<licenseUrl>https://github.com/bitwarden/cli/blob/master/LICENSE.txt</licenseUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<tags>bitwarden password manager cli</tags>
<summary>A secure and free password manager for all of your devices.</summary>
<description>
For CLI documentation, please visit https://help.bitwarden.com/article/cli/
-----------------
Bitwarden is the easiest and safest way to store all of your logins and passwords while conveniently keeping them synced between all of your devices.
Password theft is a serious problem. The websites and apps that you use are under attack every day. Security breaches occur and your passwords are stolen. When you reuse the same passwords across apps and websites hackers can easily access your email, bank, and other important accounts.
Security experts recommend that you use a different, randomly generated password for every account that you create. But how do you manage all those passwords? Bitwarden makes it easy for you to create, store, and access your passwords.
Bitwarden stores all of your logins in an encrypted vault that syncs across all of your devices. Since it's fully encrypted before it ever leaves your device, only you have access to your data. Not even the team at Bitwarden can read your data, even if we wanted to. Your data is sealed with AES-256 bit encryption, salted hashing, and PBKDF2 SHA-256.
Bitwarden is 100% open source software. The source code for Bitwarden is hosted on GitHub and everyone is free to review, audit, and contribute to the Bitwarden codebase.
</description>
</metadata>
<files>
<file src="tools\**" target="tools" />
</files>
</package>

View File

@ -0,0 +1,6 @@
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.
This package is published by the Bitwarden project itself. Any binaries will
be identical to other package types published by the project.

View File

@ -0,0 +1,17 @@
name: bw
version: __version__
summary: Bitwarden CLI
description: A secure and free password manager for all of your devices.
confinement: strict
base: core18
apps:
bw:
command: bw
plugs: [network, home, network-bind]
parts:
bw:
plugin: dump
source: ./bw-linux-$SNAPCRAFT_PROJECT_VERSION.zip
override-build: |
chmod +x bw
snapcraftctl build

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