Merge branch 'master' into bug/ps-136-search-accented-characters-clients
This commit is contained in:
commit
f345eca0d0
|
@ -7,3 +7,9 @@
|
|||
521feae535d83166e620c3c28dfc3e7b0314a00e
|
||||
# Browser: Monorepository https://github.com/bitwarden/clients/commit/7712ca6fa505c4b80b168258a7fc6a02c13160fd
|
||||
28bc4113b9bbae4dba2b5af14d460764fce79acf
|
||||
|
||||
# CLI: Apply Prettier https://github.com/bitwarden/cli/pull/426
|
||||
910b4a24e649f21acbf4da5b2d422b121d514bd5
|
||||
|
||||
# CLI: Monorepository https://github.com/bitwarden/clients/commit/980429f4bdcb178d8d92d8202cbdacfaa45c917e
|
||||
980429f4bdcb178d8d92d8202cbdacfaa45c917e
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
name: CLI Bug Report
|
||||
description: File a bug report
|
||||
labels: [bug, cli]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: How can we reproduce the behavior.
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. Click on '...'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Result
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Result
|
||||
description: A clear and concise description of what is happening.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots or Videos
|
||||
description: If applicable, add screenshots and/or a short video to help explain your problem.
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: What operating system are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What version of the operating system(s) are you seeing the problem on?
|
||||
- type: dropdown
|
||||
id: shell
|
||||
attributes:
|
||||
label: Shell
|
||||
description: What shell(s) are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Bash
|
||||
- Zsh
|
||||
- PowerShell
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Build Version
|
||||
description: What version of our software are you running? (run `bw --version`)
|
||||
validations:
|
||||
required: true
|
|
@ -10,9 +10,10 @@ on:
|
|||
- 'apps/browser/**'
|
||||
- '.github/workflows/build-browser.yml'
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
branches:
|
||||
- 'master'
|
||||
- 'rc'
|
||||
- 'hotfix-rc/**'
|
||||
paths:
|
||||
- 'apps/browser/**'
|
||||
- '.github/workflows/build-browser.yml'
|
||||
|
|
|
@ -10,9 +10,10 @@ on:
|
|||
- 'apps/desktop/**'
|
||||
- '.github/workflows/build-desktop.yml'
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
branches:
|
||||
- 'master'
|
||||
- 'rc'
|
||||
- 'hotfix-rc/**'
|
||||
paths:
|
||||
- 'apps/desktop/**'
|
||||
- '.github/workflows/build-desktop.yml'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,5 +16,8 @@ apps/desktop/dist-safari
|
|||
apps/desktop/desktop_native
|
||||
apps/desktop/src/scripts/duo.js
|
||||
|
||||
apps/cli/src/locales
|
||||
apps/cli/.github
|
||||
|
||||
# Github Workflows
|
||||
.github/workflows
|
||||
|
|
21
README.md
21
README.md
|
@ -69,3 +69,24 @@ git merge clients/master
|
|||
|
||||
# Push to clients or your own fork
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```
|
||||
# Merge master
|
||||
git merge master
|
||||
|
||||
# Merge branch mono-repo-prep
|
||||
git merge 980429f4bdcb178d8d92d8202cbdacfaa45c917e
|
||||
|
||||
# Verify files are placed in apps/cli
|
||||
|
||||
# Add remote
|
||||
git remote add clients git@github.com:bitwarden/clients.git
|
||||
|
||||
# Merge against clients master
|
||||
git fetch clients
|
||||
git merge clients/master
|
||||
|
||||
# Push to clients or your own fork
|
||||
```
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
|
@ -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 }}
|
|
@ -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"
|
|
@ -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
|
|
@ -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 }}"
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
build
|
||||
dist
|
||||
|
||||
config/local.json
|
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!/build
|
||||
!/build/**/*
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"debug.javascript.terminalOptions": {
|
||||
"sourceMapPathOverrides": {
|
||||
"meteor://💻app/*": "${workspaceFolder}/*",
|
||||
"webpack:///./~/*": "${workspaceFolder}/node_modules/*",
|
||||
"webpack://?:*/*": "${workspaceFolder}/*",
|
||||
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
[![Github Workflow build on master](https://github.com/bitwarden/clients/actions/workflows/build-cli.yml/badge.svg?branch=master)](https://github.com/bitwarden/clients/actions/workflows/build-cli.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
|
||||
```
|
|
@ -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,
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"flags": {
|
||||
"serve": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"flags": {
|
||||
"serve": true
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 1370006f6ea310cf85a12bcbd8213f74f9552c4d
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"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",
|
||||
"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",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^10.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jasmine": "^3.7.0",
|
||||
"jasmine-core": "^3.7.1",
|
||||
"jasmine-ts": "^0.4.0",
|
||||
"jasmine-ts-console-reporter": "^3.1.1",
|
||||
"pkg": "5.7.0",
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
describe("bw", () => {
|
||||
it("is a placeholder test");
|
||||
});
|
|
@ -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());
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": ["**/*[sS]pec.ts"],
|
||||
"helpers": ["helpers.ts"],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig",
|
||||
"include": ["src", "spec"],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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();
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export type Flags = {
|
||||
serve?: boolean;
|
||||
};
|
||||
|
||||
export type FlagName = keyof Flags;
|
|
@ -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."
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t 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>
|
|
@ -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.
|
|
@ -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
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2016",
|
||||
"module": "es6",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"jslib-common/*": ["jslib/common/src/*"],
|
||||
"jslib-node/*": ["jslib/node/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const nodeExternals = require("webpack-node-externals");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const config = require("./config/config");
|
||||
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = "development";
|
||||
}
|
||||
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
||||
|
||||
const envConfig = config.load(ENV);
|
||||
config.log(envConfig);
|
||||
|
||||
const moduleRules = [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: "ts-loader",
|
||||
exclude: path.resolve(__dirname, "node_modules"),
|
||||
},
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new CleanWebpackPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: "./src/locales", to: "locales" }],
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.BWCLI_ENV": JSON.stringify(ENV),
|
||||
}),
|
||||
new webpack.BannerPlugin({
|
||||
banner: "#!/usr/bin/env node",
|
||||
raw: true,
|
||||
}),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /^encoding$/,
|
||||
contextRegExp: /node-fetch/,
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
BWCLI_ENV: ENV,
|
||||
FLAGS: envConfig.flags,
|
||||
}),
|
||||
];
|
||||
|
||||
const webpackConfig = {
|
||||
mode: ENV,
|
||||
target: "node",
|
||||
devtool: ENV === "development" ? "eval-source-map" : "source-map",
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false,
|
||||
},
|
||||
entry: {
|
||||
bw: "./src/bw.ts",
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("node_modules")],
|
||||
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
},
|
||||
module: { rules: moduleRules },
|
||||
plugins: plugins,
|
||||
externals: [nodeExternals()],
|
||||
};
|
||||
|
||||
module.exports = webpackConfig;
|
Loading…
Reference in New Issue