1
0
mirror of https://github.com/bitwarden/cli synced 2025-12-06 04:23:19 +00:00

Apply Prettier (#426)

This commit is contained in:
Oscar Hinton
2021-12-20 18:04:00 +01:00
committed by GitHub
parent ec53a16c00
commit 910b4a24e6
59 changed files with 4630 additions and 4117 deletions

1
.gitattributes vendored Normal file
View File

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

View File

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

View File

@@ -129,8 +129,8 @@ jobs:
- name: Install - name: Install
run: npm ci run: npm ci
# - name: Run linter - name: Run linter
# run: npm run lint run: npm run lint
- name: Build & Package - name: Build & Package
run: npm run dist run: npm run dist

28
.vscode/launch.json vendored
View File

@@ -1,18 +1,14 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Program", "name": "Launch Program",
"protocol": "inspector", "protocol": "inspector",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"program": "${workspaceFolder}/build/bw.js", "program": "${workspaceFolder}/build/bw.js",
"args": [ "args": ["login", "sdfsd@sdfdf.com", "ddddddd"]
"login", }
"sdfsd@sdfdf.com", ]
"ddddddd"
]
}
]
} }

View File

@@ -6,15 +6,11 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
Here is how you can get involved: Here is how you can get involved:
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one - **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code - **Report a bug or submit a bugfix:** Use Github issues and pull requests
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Report a bug or submit a bugfix:** Use Github issues and pull requests - **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
## Contributor Agreement ## Contributor Agreement
@@ -22,6 +18,6 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/cli)
## Pull Request Guidelines ## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request - use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch - commit any pull requests against the `master` branch
* include a link to your Community Forums post - include a link to your Community Forums post

View File

@@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every - Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue. effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a - Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate. third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or - Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder. account holder.

510
src/bw.ts
View File

@@ -1,228 +1,334 @@
import * as program from 'commander'; import * as program from "commander";
import * as fs from 'fs'; import * as fs from "fs";
import * as jsdom from 'jsdom'; import * as jsdom from "jsdom";
import * as path from 'path'; import * as path from "path";
import { LogLevelType } from 'jslib-common/enums/logLevelType'; import { LogLevelType } from "jslib-common/enums/logLevelType";
import { AuthService } from 'jslib-common/services/auth.service'; import { AuthService } from "jslib-common/services/auth.service";
import { I18nService } from './services/i18n.service'; import { I18nService } from "./services/i18n.service";
import { NodeEnvSecureStorageService } from './services/nodeEnvSecureStorage.service'; import { NodeEnvSecureStorageService } from "./services/nodeEnvSecureStorage.service";
import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service'; import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.service'; import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
import { AppIdService } from 'jslib-common/services/appId.service'; import { AppIdService } from "jslib-common/services/appId.service";
import { AuditService } from 'jslib-common/services/audit.service'; import { AuditService } from "jslib-common/services/audit.service";
import { CipherService } from 'jslib-common/services/cipher.service'; import { CipherService } from "jslib-common/services/cipher.service";
import { CollectionService } from 'jslib-common/services/collection.service'; import { CollectionService } from "jslib-common/services/collection.service";
import { ConstantsService } from 'jslib-common/services/constants.service'; import { ConstantsService } from "jslib-common/services/constants.service";
import { ContainerService } from 'jslib-common/services/container.service'; import { ContainerService } from "jslib-common/services/container.service";
import { CryptoService } from 'jslib-common/services/crypto.service'; import { CryptoService } from "jslib-common/services/crypto.service";
import { EnvironmentService } from 'jslib-common/services/environment.service'; import { EnvironmentService } from "jslib-common/services/environment.service";
import { ExportService } from 'jslib-common/services/export.service'; import { ExportService } from "jslib-common/services/export.service";
import { FileUploadService } from 'jslib-common/services/fileUpload.service'; import { FileUploadService } from "jslib-common/services/fileUpload.service";
import { FolderService } from 'jslib-common/services/folder.service'; import { FolderService } from "jslib-common/services/folder.service";
import { ImportService } from 'jslib-common/services/import.service'; import { ImportService } from "jslib-common/services/import.service";
import { KeyConnectorService } from 'jslib-common/services/keyConnector.service'; import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
import { NoopMessagingService } from 'jslib-common/services/noopMessaging.service'; import { NoopMessagingService } from "jslib-common/services/noopMessaging.service";
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
import { PolicyService } from 'jslib-common/services/policy.service'; import { PolicyService } from "jslib-common/services/policy.service";
import { SearchService } from 'jslib-common/services/search.service'; import { SearchService } from "jslib-common/services/search.service";
import { SendService } from 'jslib-common/services/send.service'; import { SendService } from "jslib-common/services/send.service";
import { SettingsService } from 'jslib-common/services/settings.service'; import { SettingsService } from "jslib-common/services/settings.service";
import { SyncService } from 'jslib-common/services/sync.service'; import { SyncService } from "jslib-common/services/sync.service";
import { TokenService } from 'jslib-common/services/token.service'; import { TokenService } from "jslib-common/services/token.service";
import { TotpService } from 'jslib-common/services/totp.service'; import { TotpService } from "jslib-common/services/totp.service";
import { UserService } from 'jslib-common/services/user.service'; import { UserService } from "jslib-common/services/user.service";
import { UserVerificationService } from 'jslib-common/services/userVerification.service'; import { UserVerificationService } from "jslib-common/services/userVerification.service";
import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { LowdbStorageService } from 'jslib-node/services/lowdbStorage.service'; import { LowdbStorageService } from "jslib-node/services/lowdbStorage.service";
import { NodeApiService } from 'jslib-node/services/nodeApi.service'; import { NodeApiService } from "jslib-node/services/nodeApi.service";
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service'; import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
import { Program } from './program'; import { Program } from "./program";
import { SendProgram } from './send.program'; import { SendProgram } from "./send.program";
import { VaultProgram } from './vault.program'; import { VaultProgram } from "./vault.program";
// Polyfills // Polyfills
(global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser;
// tslint:disable-next-line // tslint:disable-next-line
const packageJson = require('../package.json'); const packageJson = require("../package.json");
export class Main { export class Main {
messagingService: NoopMessagingService; messagingService: NoopMessagingService;
storageService: LowdbStorageService; storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService; secureStorageService: NodeEnvSecureStorageService;
i18nService: I18nService; i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService; platformUtilsService: CliPlatformUtilsService;
constantsService: ConstantsService; constantsService: ConstantsService;
cryptoService: CryptoService; cryptoService: CryptoService;
tokenService: TokenService; tokenService: TokenService;
appIdService: AppIdService; appIdService: AppIdService;
apiService: NodeApiService; apiService: NodeApiService;
environmentService: EnvironmentService; environmentService: EnvironmentService;
userService: UserService; userService: UserService;
settingsService: SettingsService; settingsService: SettingsService;
cipherService: CipherService; cipherService: CipherService;
folderService: FolderService; folderService: FolderService;
collectionService: CollectionService; collectionService: CollectionService;
vaultTimeoutService: VaultTimeoutService; vaultTimeoutService: VaultTimeoutService;
syncService: SyncService; syncService: SyncService;
passwordGenerationService: PasswordGenerationService; passwordGenerationService: PasswordGenerationService;
totpService: TotpService; totpService: TotpService;
containerService: ContainerService; containerService: ContainerService;
auditService: AuditService; auditService: AuditService;
importService: ImportService; importService: ImportService;
exportService: ExportService; exportService: ExportService;
searchService: SearchService; searchService: SearchService;
cryptoFunctionService: NodeCryptoFunctionService; cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService; authService: AuthService;
policyService: PolicyService; policyService: PolicyService;
program: Program; program: Program;
vaultProgram: VaultProgram; vaultProgram: VaultProgram;
sendProgram: SendProgram; sendProgram: SendProgram;
logService: ConsoleLogService; logService: ConsoleLogService;
sendService: SendService; sendService: SendService;
fileUploadService: FileUploadService; fileUploadService: FileUploadService;
keyConnectorService: KeyConnectorService; keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService; userVerificationService: UserVerificationService;
constructor() { constructor() {
let p = null; let p = null;
const relativeDataDir = path.join(path.dirname(process.execPath), 'bw-data'); const relativeDataDir = path.join(path.dirname(process.execPath), "bw-data");
if (fs.existsSync(relativeDataDir)) { if (fs.existsSync(relativeDataDir)) {
p = relativeDataDir; p = relativeDataDir;
} else if (process.env.BITWARDENCLI_APPDATA_DIR) { } else if (process.env.BITWARDENCLI_APPDATA_DIR) {
p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR); p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR);
} else if (process.platform === 'darwin') { } else if (process.platform === "darwin") {
p = path.join(process.env.HOME, 'Library/Application Support/Bitwarden CLI'); p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI");
} else if (process.platform === 'win32') { } else if (process.platform === "win32") {
p = path.join(process.env.APPDATA, 'Bitwarden CLI'); p = path.join(process.env.APPDATA, "Bitwarden CLI");
} else if (process.env.XDG_CONFIG_HOME) { } else if (process.env.XDG_CONFIG_HOME) {
p = path.join(process.env.XDG_CONFIG_HOME, 'Bitwarden CLI'); p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI");
} else { } else {
p = path.join(process.env.HOME, '.config/Bitwarden CLI'); p = path.join(process.env.HOME, ".config/Bitwarden CLI");
}
this.i18nService = new I18nService('en', './locales');
this.platformUtilsService = new CliPlatformUtilsService('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, true);
this.secureStorageService = new NodeEnvSecureStorageService(this.storageService, this.logService,
() => this.cryptoService);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
this.cryptoFunctionService, this.platformUtilsService, this.logService);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.storageService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.storageService);
this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService, this.environmentService,
async (expired: boolean) => await this.logout(),
'Bitwarden_CLI/' + this.platformUtilsService.getApplicationVersionSync() +
' (' + this.platformUtilsService.getDeviceString().toUpperCase() + ')', (clientId, clientSecret) =>
this.authService.logInApiKey(clientId, clientSecret));
this.userService = new UserService(this.tokenService, this.storageService);
this.containerService = new ContainerService(this.cryptoService);
this.settingsService = new SettingsService(this.userService, this.storageService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService,
this.apiService, this.fileUploadService, this.storageService, this.i18nService, null, this.logService);
this.folderService = new FolderService(this.cryptoService, this.userService, this.apiService,
this.storageService, this.i18nService, this.cipherService);
this.collectionService = new CollectionService(this.cryptoService, this.userService, this.storageService,
this.i18nService);
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
this.policyService = new PolicyService(this.userService, this.storageService, this.apiService);
this.sendService = new SendService(this.cryptoService, this.userService, this.apiService, this.fileUploadService,
this.storageService, this.i18nService, this.cryptoFunctionService);
this.keyConnectorService = new KeyConnectorService(this.storageService, this.userService, this.cryptoService,
this.apiService, this.tokenService, this.logService);
this.vaultTimeoutService = new VaultTimeoutService(this.cipherService, this.folderService,
this.collectionService, this.cryptoService, this.platformUtilsService, this.storageService,
this.messagingService, this.searchService, this.userService, this.tokenService, this.policyService,
this.keyConnectorService, async () => await this.cryptoService.clearStoredKey('auto'), null);
this.syncService = new SyncService(this.userService, this.apiService, this.settingsService,
this.folderService, this.cipherService, this.cryptoService, this.collectionService,
this.storageService, this.messagingService, this.policyService, this.sendService,
this.logService, this.tokenService, this.keyConnectorService,
async (expired: boolean) => await this.logout());
this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService,
this.policyService);
this.totpService = new TotpService(this.storageService, this.cryptoFunctionService, this.logService);
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.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService,
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService,
this.vaultTimeoutService, this.logService, this.cryptoFunctionService, this.environmentService,
this.keyConnectorService, true);
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() { this.i18nService = new I18nService("en", "./locales");
await this.init(); this.platformUtilsService = new CliPlatformUtilsService("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, true);
this.secureStorageService = new NodeEnvSecureStorageService(
this.storageService,
this.logService,
() => this.cryptoService
);
this.cryptoService = new CryptoService(
this.storageService,
this.secureStorageService,
this.cryptoFunctionService,
this.platformUtilsService,
this.logService
);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.storageService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.storageService);
this.apiService = new NodeApiService(
this.tokenService,
this.platformUtilsService,
this.environmentService,
async (expired: boolean) => await this.logout(),
"Bitwarden_CLI/" +
this.platformUtilsService.getApplicationVersionSync() +
" (" +
this.platformUtilsService.getDeviceString().toUpperCase() +
")",
(clientId, clientSecret) => this.authService.logInApiKey(clientId, clientSecret)
);
this.userService = new UserService(this.tokenService, this.storageService);
this.containerService = new ContainerService(this.cryptoService);
this.settingsService = new SettingsService(this.userService, this.storageService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(
this.cryptoService,
this.userService,
this.settingsService,
this.apiService,
this.fileUploadService,
this.storageService,
this.i18nService,
null,
this.logService
);
this.folderService = new FolderService(
this.cryptoService,
this.userService,
this.apiService,
this.storageService,
this.i18nService,
this.cipherService
);
this.collectionService = new CollectionService(
this.cryptoService,
this.userService,
this.storageService,
this.i18nService
);
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
this.policyService = new PolicyService(this.userService, this.storageService, this.apiService);
this.sendService = new SendService(
this.cryptoService,
this.userService,
this.apiService,
this.fileUploadService,
this.storageService,
this.i18nService,
this.cryptoFunctionService
);
this.keyConnectorService = new KeyConnectorService(
this.storageService,
this.userService,
this.cryptoService,
this.apiService,
this.tokenService,
this.logService
);
this.vaultTimeoutService = new VaultTimeoutService(
this.cipherService,
this.folderService,
this.collectionService,
this.cryptoService,
this.platformUtilsService,
this.storageService,
this.messagingService,
this.searchService,
this.userService,
this.tokenService,
this.policyService,
this.keyConnectorService,
async () => await this.cryptoService.clearStoredKey("auto"),
null
);
this.syncService = new SyncService(
this.userService,
this.apiService,
this.settingsService,
this.folderService,
this.cipherService,
this.cryptoService,
this.collectionService,
this.storageService,
this.messagingService,
this.policyService,
this.sendService,
this.logService,
this.tokenService,
this.keyConnectorService,
async (expired: boolean) => await this.logout()
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.storageService,
this.policyService
);
this.totpService = new TotpService(
this.storageService,
this.cryptoFunctionService,
this.logService
);
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.authService = new AuthService(
this.cryptoService,
this.apiService,
this.userService,
this.tokenService,
this.appIdService,
this.i18nService,
this.platformUtilsService,
this.messagingService,
this.vaultTimeoutService,
this.logService,
this.cryptoFunctionService,
this.environmentService,
this.keyConnectorService,
true
);
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
);
}
await this.program.register(); async run() {
await this.vaultProgram.register(); await this.init();
await this.sendProgram.register();
program await this.program.register();
.parse(process.argv); await this.vaultProgram.register();
await this.sendProgram.register();
if (process.argv.slice(2).length === 0) { program.parse(process.argv);
program.outputHelp();
}
if (process.argv.slice(2).length === 0) {
program.outputHelp();
} }
}
async logout() { async logout() {
const userId = await this.userService.getUserId(); const userId = await this.userService.getUserId();
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.tokenService.clearToken(), this.tokenService.clearToken(),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.userService.clear(), this.userService.clear(),
this.settingsService.clear(userId), this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),
this.policyService.clear(userId), this.policyService.clear(userId),
this.passwordGenerationService.clear(), this.passwordGenerationService.clear(),
]); ]);
process.env.BW_SESSION = null; process.env.BW_SESSION = null;
} }
private async init() { private async init() {
await this.storageService.init(); await this.storageService.init();
this.containerService.attachToWindow(global); this.containerService.attachToWindow(global);
await this.environmentService.setUrlsFromStorage(); await this.environmentService.setUrlsFromStorage();
// Dev Server URLs. Comment out the line above. // Dev Server URLs. Comment out the line above.
// this.apiService.setUrls({ // this.apiService.setUrls({
// base: null, // base: null,
// api: 'http://localhost:4000', // api: 'http://localhost:4000',
// identity: 'http://localhost:33656', // identity: 'http://localhost:33656',
// }); // });
const locale = await this.storageService.get<string>(ConstantsService.localeKey); const locale = await this.storageService.get<string>(ConstantsService.localeKey);
await this.i18nService.init(locale); await this.i18nService.init(locale);
this.authService.init(); this.authService.init();
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey); const installedVersion = await this.storageService.get<string>(
const currentVersion = await this.platformUtilsService.getApplicationVersion(); ConstantsService.installedVersionKey
if (installedVersion == null || installedVersion !== currentVersion) { );
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion); const currentVersion = await this.platformUtilsService.getApplicationVersion();
} if (installedVersion == null || installedVersion !== currentVersion) {
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
} }
}
} }
const main = new Main(); const main = new Main();

View File

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

View File

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

View File

@@ -1,58 +1,58 @@
import * as program from 'commander'; import * as program from "commander";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { OrganizationUserConfirmRequest } from 'jslib-common/models/request/organizationUserConfirmRequest'; import { OrganizationUserConfirmRequest } from "jslib-common/models/request/organizationUserConfirmRequest";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class ConfirmCommand { export class ConfirmCommand {
constructor(private apiService: ApiService, private cryptoService: CryptoService) { } constructor(private apiService: ApiService, private cryptoService: CryptoService) {}
async run(object: string, id: string, cmd: program.Command): Promise<Response> { async run(object: string, id: string, cmd: program.Command): Promise<Response> {
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
}
switch (object.toLowerCase()) {
case 'org-member':
return await this.confirmOrganizationMember(id, cmd);
default:
return Response.badRequest('Unknown object.');
}
} }
private async confirmOrganizationMember(id: string, options: program.OptionValues) { switch (object.toLowerCase()) {
if (options.organizationid == null || options.organizationid === '') { case "org-member":
return Response.badRequest('--organizationid <organizationid> required.'); return await this.confirmOrganizationMember(id, cmd);
} default:
if (!Utils.isGuid(id)) { return Response.badRequest("Unknown object.");
return Response.error('`' + id + '` is not a GUID.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + 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);
}
} }
}
private async confirmOrganizationMember(id: string, options: program.OptionValues) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + 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);
}
}
} }

View File

@@ -1,166 +1,180 @@
import * as program from 'commander'; import * as program from "commander";
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; import * as path from "path";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { Cipher } from 'jslib-common/models/export/cipher'; import { Cipher } from "jslib-common/models/export/cipher";
import { Collection } from 'jslib-common/models/export/collection'; import { Collection } from "jslib-common/models/export/collection";
import { Folder } from 'jslib-common/models/export/folder'; import { Folder } from "jslib-common/models/export/folder";
import { CollectionRequest } from 'jslib-common/models/request/collectionRequest'; import { CollectionRequest } from "jslib-common/models/request/collectionRequest";
import { SelectionReadOnlyRequest } from 'jslib-common/models/request/selectionReadOnlyRequest'; import { SelectionReadOnlyRequest } from "jslib-common/models/request/selectionReadOnlyRequest";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { CipherResponse } from '../models/response/cipherResponse'; import { CipherResponse } from "../models/response/cipherResponse";
import { FolderResponse } from '../models/response/folderResponse'; import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from '../models/response/organizationCollectionResponse'; import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { OrganizationCollectionRequest } from '../models/request/organizationCollectionRequest'; import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CliUtils } from '../utils'; import { CliUtils } from "../utils";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class CreateCommand { export class CreateCommand {
constructor(private cipherService: CipherService, private folderService: FolderService, constructor(
private userService: UserService, private cryptoService: CryptoService, private cipherService: CipherService,
private apiService: ApiService) { } private folderService: FolderService,
private userService: UserService,
private cryptoService: CryptoService,
private apiService: ApiService
) {}
async run(object: string, requestJson: string, cmd: program.Command): Promise<Response> { async run(object: string, requestJson: string, cmd: program.Command): Promise<Response> {
let req: any = null; let req: any = null;
if (object !== 'attachment') { if (object !== "attachment") {
if (requestJson == null || requestJson === '') { if (requestJson == null || requestJson === "") {
requestJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
} }
if (requestJson == null || requestJson === '') { if (requestJson == null || requestJson === "") {
return Response.badRequest('`requestJson` was not provided.'); return Response.badRequest("`requestJson` was not provided.");
} }
try { try {
const reqJson = Buffer.from(requestJson, 'base64').toString(); const reqJson = Buffer.from(requestJson, "base64").toString();
req = JSON.parse(reqJson); req = JSON.parse(reqJson);
} catch (e) { } catch (e) {
return Response.badRequest('Error parsing the encoded request data.'); return Response.badRequest("Error parsing the encoded request data.");
} }
}
switch (object.toLowerCase()) {
case 'item':
return await this.createCipher(req);
case 'attachment':
return await this.createAttachment(cmd);
case 'folder':
return await this.createFolder(req);
case 'org-collection':
return await this.createOrganizationCollection(req, cmd);
default:
return Response.badRequest('Unknown object.');
}
} }
private async createCipher(req: Cipher) { switch (object.toLowerCase()) {
const cipher = await this.cipherService.encrypt(Cipher.toView(req)); case "item":
try { return await this.createCipher(req);
await this.cipherService.saveWithServer(cipher); case "attachment":
const newCipher = await this.cipherService.get(cipher.id); return await this.createAttachment(cmd);
const decCipher = await newCipher.decrypt(); case "folder":
const res = new CipherResponse(decCipher); return await this.createFolder(req);
return Response.success(res); case "org-collection":
} catch (e) { return await this.createOrganizationCollection(req, cmd);
return Response.error(e); default:
} return Response.badRequest("Unknown object.");
}
}
private async createCipher(req: Cipher) {
const cipher = await this.cipherService.encrypt(Cipher.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: program.OptionValues) {
if (options.itemid == null || options.itemid === "") {
return Response.badRequest("--itemid <itemid> required.");
}
if (options.file == null || options.file === "") {
return Response.badRequest("--file <file> required.");
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(options.file)) {
return Response.badRequest("Cannot find file at " + filePath);
} }
private async createAttachment(options: program.OptionValues) { const itemId = options.itemid.toLowerCase();
if (options.itemid == null || options.itemid === '') { const cipher = await this.cipherService.get(itemId);
return Response.badRequest('--itemid <itemid> required.'); if (cipher == null) {
} return Response.notFound();
if (options.file == null || options.file === '') {
return Response.badRequest('--file <file> required.');
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(options.file)) {
return Response.badRequest('Cannot find file at ' + filePath);
}
const itemId = options.itemid.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
}
if (cipher.organizationId == null && !(await this.userService.canAccessPremium())) {
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 {
const fileBuf = fs.readFileSync(filePath);
await this.cipherService.saveAttachmentRawWithServer(cipher, path.basename(filePath),
new Uint8Array(fileBuf).buffer);
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 createFolder(req: Folder) { if (cipher.organizationId == null && !(await this.userService.canAccessPremium())) {
const folder = await this.folderService.encrypt(Folder.toView(req)); return Response.error("Premium status is required to use this feature.");
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: program.OptionValues) { const encKey = await this.cryptoService.getEncKey();
if (options.organizationid == null || options.organizationid === '') { if (encKey == null) {
return Response.badRequest('--organizationid <organizationid> required.'); return Response.error(
} "You must update your encryption key before you can use this feature. " +
if (!Utils.isGuid(options.organizationid)) { "See https://help.bitwarden.com/article/update-encryption-key/"
return Response.error('`' + options.organizationid + '` is not a GUID.'); );
}
if (options.organizationid !== req.organizationId) {
return Response.error('--organizationid <organizationid> 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 = Collection.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
} }
try {
const fileBuf = fs.readFileSync(filePath);
await this.cipherService.saveAttachmentRawWithServer(
cipher,
path.basename(filePath),
new Uint8Array(fileBuf).buffer
);
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 createFolder(req: Folder) {
const folder = await this.folderService.encrypt(Folder.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: program.OptionValues
) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + options.organizationid + "` is not a GUID.");
}
if (options.organizationid !== req.organizationId) {
return Response.error("--organizationid <organizationid> 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 = Collection.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
} }

View File

@@ -1,116 +1,120 @@
import * as program from 'commander'; import * as program from "commander";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class DeleteCommand { export class DeleteCommand {
constructor(private cipherService: CipherService, private folderService: FolderService, constructor(
private userService: UserService, private apiService: ApiService) { } private cipherService: CipherService,
private folderService: FolderService,
private userService: UserService,
private apiService: ApiService
) {}
async run(object: string, id: string, cmd: program.Command): Promise<Response> { async run(object: string, id: string, cmd: program.Command): Promise<Response> {
if (id != null) { if (id != null) {
id = id.toLowerCase(); id = id.toLowerCase();
}
switch (object.toLowerCase()) {
case 'item':
return await this.deleteCipher(id, cmd);
case 'attachment':
return await this.deleteAttachment(id, cmd);
case 'folder':
return await this.deleteFolder(id);
case 'org-collection':
return await this.deleteOrganizationCollection(id, cmd);
default:
return Response.badRequest('Unknown object.');
}
} }
private async deleteCipher(id: string, options: program.OptionValues) { switch (object.toLowerCase()) {
const cipher = await this.cipherService.get(id); case "item":
if (cipher == null) { return await this.deleteCipher(id, cmd);
return Response.notFound(); case "attachment":
} return await this.deleteAttachment(id, cmd);
case "folder":
return await this.deleteFolder(id);
case "org-collection":
return await this.deleteOrganizationCollection(id, cmd);
default:
return Response.badRequest("Unknown object.");
}
}
try { private async deleteCipher(id: string, options: program.OptionValues) {
if (options.permanent) { const cipher = await this.cipherService.get(id);
await this.cipherService.deleteWithServer(id); if (cipher == null) {
} else { return Response.notFound();
await this.cipherService.softDeleteWithServer(id);
}
return Response.success();
} catch (e) {
return Response.error(e);
}
} }
private async deleteAttachment(id: string, options: program.OptionValues) { try {
if (options.itemid == null || options.itemid === '') { if (options.permanent) {
return Response.badRequest('--itemid <itemid> required.'); await this.cipherService.deleteWithServer(id);
} } else {
await this.cipherService.softDeleteWithServer(id);
}
return Response.success();
} catch (e) {
return Response.error(e);
}
}
const itemId = options.itemid.toLowerCase(); private async deleteAttachment(id: string, options: program.OptionValues) {
const cipher = await this.cipherService.get(itemId); if (options.itemid == null || options.itemid === "") {
if (cipher == null) { return Response.badRequest("--itemid <itemid> required.");
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.userService.canAccessPremium())) {
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 itemId = options.itemid.toLowerCase();
const folder = await this.folderService.get(id); const cipher = await this.cipherService.get(itemId);
if (folder == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
}
try {
await this.folderService.deleteWithServer(id);
return Response.success();
} catch (e) {
return Response.error(e);
}
} }
private async deleteOrganizationCollection(id: string, options: program.OptionValues) { if (cipher.attachments == null || cipher.attachments.length === 0) {
if (options.organizationid == null || options.organizationid === '') { return Response.error("No attachments available for this item.");
return Response.badRequest('--organizationid <organizationid> required.');
}
if (!Utils.isGuid(id)) {
return Response.error('`' + id + '` is not a GUID.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
}
try {
await this.apiService.deleteCollection(options.organizationid, id);
return Response.success();
} catch (e) {
return Response.error(e);
}
} }
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.userService.canAccessPremium())) {
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: program.OptionValues) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + options.organizationid + "` is not a GUID.");
}
try {
await this.apiService.deleteCollection(options.organizationid, id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
} }

View File

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

View File

@@ -1,164 +1,183 @@
import * as program from 'commander'; import * as program from "commander";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { Cipher } from 'jslib-common/models/export/cipher'; import { Cipher } from "jslib-common/models/export/cipher";
import { Collection } from 'jslib-common/models/export/collection'; import { Collection } from "jslib-common/models/export/collection";
import { Folder } from 'jslib-common/models/export/folder'; import { Folder } from "jslib-common/models/export/folder";
import { CollectionRequest } from 'jslib-common/models/request/collectionRequest'; import { CollectionRequest } from "jslib-common/models/request/collectionRequest";
import { SelectionReadOnlyRequest } from 'jslib-common/models/request/selectionReadOnlyRequest'; import { SelectionReadOnlyRequest } from "jslib-common/models/request/selectionReadOnlyRequest";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { CipherResponse } from '../models/response/cipherResponse'; import { CipherResponse } from "../models/response/cipherResponse";
import { FolderResponse } from '../models/response/folderResponse'; import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from '../models/response/organizationCollectionResponse'; import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { OrganizationCollectionRequest } from '../models/request/organizationCollectionRequest'; import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { CliUtils } from '../utils'; import { CliUtils } from "../utils";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class EditCommand { export class EditCommand {
constructor(private cipherService: CipherService, private folderService: FolderService, constructor(
private cryptoService: CryptoService, private apiService: ApiService) { } private cipherService: CipherService,
private folderService: FolderService,
private cryptoService: CryptoService,
private apiService: ApiService
) {}
async run(object: string, id: string, requestJson: string, cmd: program.Command): Promise<Response> { async run(
if (requestJson == null || requestJson === '') { object: string,
requestJson = await CliUtils.readStdin(); id: string,
} requestJson: string,
cmd: program.Command
if (requestJson == null || requestJson === '') { ): Promise<Response> {
return Response.badRequest('`requestJson` was not provided.'); if (requestJson == null || requestJson === "") {
} requestJson = await CliUtils.readStdin();
let req: any = null;
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();
}
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, cmd);
default:
return Response.badRequest('Unknown object.');
}
} }
private async editCipher(id: string, req: Cipher) { if (requestJson == null || requestJson === "") {
const cipher = await this.cipherService.get(id); return Response.badRequest("`requestJson` was not provided.");
if (cipher == null) {
return Response.notFound();
}
let cipherView = await cipher.decrypt();
if (cipherView.isDeleted) {
return Response.badRequest('You may not edit a deleted cipher. Use restore item <id> command first.');
}
cipherView = Cipher.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[]) { let req: any = null;
const cipher = await this.cipherService.get(id); try {
if (cipher == null) { const reqJson = Buffer.from(requestJson, "base64").toString();
return Response.notFound(); req = JSON.parse(reqJson);
} } catch (e) {
if (cipher.organizationId == null) { return Response.badRequest("Error parsing the encoded request data.");
return Response.badRequest('Item does not belong to an organization. Consider sharing 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: Folder) { if (id != null) {
const folder = await this.folderService.get(id); id = id.toLowerCase();
if (folder == null) {
return Response.notFound();
}
let folderView = await folder.decrypt();
folderView = Folder.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: program.OptionValues) { switch (object.toLowerCase()) {
if (options.organizationid == null || options.organizationid === '') { case "item":
return Response.badRequest('--organizationid <organizationid> required.'); return await this.editCipher(id, req);
} case "item-collections":
if (!Utils.isGuid(id)) { return await this.editCipherCollections(id, req);
return Response.error('`' + id + '` is not a GUID.'); case "folder":
} return await this.editFolder(id, req);
if (!Utils.isGuid(options.organizationid)) { case "org-collection":
return Response.error('`' + options.organizationid + '` is not a GUID.'); return await this.editOrganizationCollection(id, req, cmd);
} default:
if (options.organizationid !== req.organizationId) { return Response.badRequest("Unknown object.");
return Response.error('--organizationid <organizationid> 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 = Collection.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
} }
}
private async editCipher(id: string, req: Cipher) {
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 cipher. Use restore item <id> command first."
);
}
cipherView = Cipher.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 sharing 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: Folder) {
const folder = await this.folderService.get(id);
if (folder == null) {
return Response.notFound();
}
let folderView = await folder.decrypt();
folderView = Folder.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: program.OptionValues
) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + options.organizationid + "` is not a GUID.");
}
if (options.organizationid !== req.organizationId) {
return Response.error("--organizationid <organizationid> 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 = Collection.toView(req);
view.id = response.id;
const res = new OrganizationCollectionResponse(view, groups);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
} }

View File

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

View File

@@ -1,111 +1,128 @@
import * as program from 'commander'; import * as program from "commander";
import * as inquirer from 'inquirer'; import * as inquirer from "inquirer";
import { ExportService } from 'jslib-common/abstractions/export.service'; import { ExportService } from "jslib-common/abstractions/export.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service'; import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { PolicyType } from 'jslib-common/enums/policyType'; import { PolicyType } from "jslib-common/enums/policyType";
import { VerificationType } from 'jslib-common/enums/verificationType'; import { VerificationType } from "jslib-common/enums/verificationType";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { CliUtils } from '../utils'; import { CliUtils } from "../utils";
export class ExportCommand { export class ExportCommand {
constructor(private exportService: ExportService, private policyService: PolicyService, constructor(
private keyConnectorService: KeyConnectorService, private userVerificationService: UserVerificationService) { } private exportService: ExportService,
private policyService: PolicyService,
private keyConnectorService: KeyConnectorService,
private userVerificationService: UserVerificationService
) {}
async run(password: string, options: program.OptionValues): Promise<Response> { async run(password: string, options: program.OptionValues): Promise<Response> {
if (options.organizationid == null && if (
await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport)) { options.organizationid == null &&
return Response.badRequest( (await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport))
'One or more organization policies prevents you from exporting your personal vault.' ) {
); return Response.badRequest(
} "One or more organization policies prevents you from exporting your personal vault."
);
const canInteract = process.env.BW_NOINTERACTION !== 'true';
if (!canInteract) {
return Response.badRequest('User verification is required. Try running this command again in interactive mode.');
}
try {
await this.keyConnectorService.getUsesKeyConnector()
? await this.verifyOTP()
: await this.verifyMasterPassword(password);
} catch (e) {
return Response.badRequest(e.message);
}
let format = options.format;
if (format !== 'encrypted_json' && format !== 'json') {
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 = options.organizationid != null ?
await this.exportService.getOrganizationExport(options.organizationid, format) :
await this.exportService.getExport(format);
} catch (e) {
return Response.error(e);
}
return await this.saveFile(exportContent, options, format);
} }
async saveFile(exportContent: string, options: program.OptionValues, format: string): Promise<Response> { const canInteract = process.env.BW_NOINTERACTION !== "true";
try { if (!canInteract) {
const fileName = this.getFileName(format, options.organizationid != null ? 'org' : null); return Response.badRequest(
return await CliUtils.saveResultToFile(exportContent, options.output, fileName); "User verification is required. Try running this command again in interactive mode."
} catch (e) { );
return Response.error(e.toString());
}
} }
private getFileName(format: string, prefix?: string) { try {
if (format === 'encrypted_json') { (await this.keyConnectorService.getUsesKeyConnector())
if (prefix == null) { ? await this.verifyOTP()
prefix = 'encrypted'; : await this.verifyMasterPassword(password);
} else { } catch (e) {
prefix = 'encrypted_' + prefix; return Response.badRequest(e.message);
}
format = 'json';
}
return this.exportService.getFileName(prefix, format);
} }
private async verifyMasterPassword(password: string) { let format = options.format;
if (password == null || password === '') { if (format !== "encrypted_json" && format !== "json") {
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ format = "csv";
type: 'password', }
name: 'password', if (options.organizationid != null && !Utils.isGuid(options.organizationid)) {
message: 'Master password:', return Response.error("`" + options.organizationid + "` is not a GUID.");
}); }
password = answer.password; let exportContent: string = null;
} try {
exportContent =
options.organizationid != null
? await this.exportService.getOrganizationExport(options.organizationid, format)
: await this.exportService.getExport(format);
} catch (e) {
return Response.error(e);
}
return await this.saveFile(exportContent, options, format);
}
await this.userVerificationService.verifyUser({ async saveFile(
type: VerificationType.MasterPassword, exportContent: string,
secret: password, options: program.OptionValues,
}); format: string
): 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: string, 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 verifyMasterPassword(password: string) {
if (password == null || password === "") {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Master password:",
});
password = answer.password;
} }
private async verifyOTP() { await this.userVerificationService.verifyUser({
await this.userVerificationService.requestOTP(); type: VerificationType.MasterPassword,
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ secret: password,
type: 'password', });
name: 'otp', }
message: 'A verification code has been emailed to you.\n Verification code:',
});
await this.userVerificationService.verifyUser({ private async verifyOTP() {
type: VerificationType.OTP, await this.userVerificationService.requestOTP();
secret: answer.otp, const answer: inquirer.Answers = await inquirer.createPromptModule({
}); output: process.stderr,
} })({
type: "password",
name: "otp",
message: "A verification code has been emailed to you.\n Verification code:",
});
await this.userVerificationService.verifyUser({
type: VerificationType.OTP,
secret: answer.otp,
});
}
} }

View File

@@ -1,45 +1,46 @@
import * as program from 'commander'; import * as program from "commander";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
export class GenerateCommand { export class GenerateCommand {
constructor(private passwordGenerationService: PasswordGenerationService) { } constructor(private passwordGenerationService: PasswordGenerationService) {}
async run(cmdOptions: program.OptionValues): Promise<Response> { async run(cmdOptions: program.OptionValues): Promise<Response> {
const options = { const options = {
uppercase: cmdOptions.uppercase || false, uppercase: cmdOptions.uppercase || false,
lowercase: cmdOptions.lowercase || false, lowercase: cmdOptions.lowercase || false,
number: cmdOptions.number || false, number: cmdOptions.number || false,
special: cmdOptions.special || false, special: cmdOptions.special || false,
length: cmdOptions.length || 14, length: cmdOptions.length || 14,
type: cmdOptions.passphrase ? 'passphrase' : 'password', type: cmdOptions.passphrase ? "passphrase" : "password",
wordSeparator: cmdOptions.separator == null ? '-' : cmdOptions.separator, wordSeparator: cmdOptions.separator == null ? "-" : cmdOptions.separator,
numWords: cmdOptions.words || 3, numWords: cmdOptions.words || 3,
capitalize: cmdOptions.capitalize || false, capitalize: cmdOptions.capitalize || false,
includeNumber: cmdOptions.includeNumber || false, includeNumber: cmdOptions.includeNumber || false,
}; };
if (!options.uppercase && !options.lowercase && !options.special && !options.number) { if (!options.uppercase && !options.lowercase && !options.special && !options.number) {
options.lowercase = true; options.lowercase = true;
options.uppercase = true; options.uppercase = true;
options.number = true; options.number = true;
}
if (options.length < 5) {
options.length = 5;
}
if (options.numWords < 3) {
options.numWords = 3;
}
if (options.wordSeparator === 'space') {
options.wordSeparator = ' ';
} else if (options.wordSeparator != null && options.wordSeparator.length > 1) {
options.wordSeparator = options.wordSeparator[0];
}
const enforcedOptions = await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options);
const password = await this.passwordGenerationService.generatePassword(enforcedOptions[0]);
const res = new StringResponse(password);
return Response.success(res);
} }
if (options.length < 5) {
options.length = 5;
}
if (options.numWords < 3) {
options.numWords = 3;
}
if (options.wordSeparator === "space") {
options.wordSeparator = " ";
} else if (options.wordSeparator != null && options.wordSeparator.length > 1) {
options.wordSeparator = options.wordSeparator[0];
}
const enforcedOptions =
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options);
const password = await this.passwordGenerationService.generatePassword(enforcedOptions[0]);
const res = new StringResponse(password);
return Response.success(res);
}
} }

View File

@@ -1,497 +1,540 @@
import * as program from 'commander'; import * as program from "commander";
import { CipherType } from 'jslib-common/enums/cipherType'; import { CipherType } from "jslib-common/enums/cipherType";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuditService } from 'jslib-common/abstractions/audit.service'; import { AuditService } from "jslib-common/abstractions/audit.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { TotpService } from 'jslib-common/abstractions/totp.service'; import { TotpService } from "jslib-common/abstractions/totp.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { Organization } from 'jslib-common/models/domain/organization'; import { Organization } from "jslib-common/models/domain/organization";
import { Card } from 'jslib-common/models/export/card'; import { Card } from "jslib-common/models/export/card";
import { Cipher } from 'jslib-common/models/export/cipher'; import { Cipher } from "jslib-common/models/export/cipher";
import { Collection } from 'jslib-common/models/export/collection'; import { Collection } from "jslib-common/models/export/collection";
import { Field } from 'jslib-common/models/export/field'; import { Field } from "jslib-common/models/export/field";
import { Folder } from 'jslib-common/models/export/folder'; import { Folder } from "jslib-common/models/export/folder";
import { Identity } from 'jslib-common/models/export/identity'; import { Identity } from "jslib-common/models/export/identity";
import { Login } from 'jslib-common/models/export/login'; import { Login } from "jslib-common/models/export/login";
import { LoginUri } from 'jslib-common/models/export/loginUri'; import { LoginUri } from "jslib-common/models/export/loginUri";
import { SecureNote } from 'jslib-common/models/export/secureNote'; import { SecureNote } from "jslib-common/models/export/secureNote";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import { CollectionView } from 'jslib-common/models/view/collectionView'; import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from 'jslib-common/models/view/folderView'; import { FolderView } from "jslib-common/models/view/folderView";
import { EncString } from 'jslib-common/models/domain/encString'; import { EncString } from "jslib-common/models/domain/encString";
import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { SendType } from 'jslib-common/enums/sendType'; import { SendType } from "jslib-common/enums/sendType";
import { CipherResponse } from '../models/response/cipherResponse'; import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionResponse } from '../models/response/collectionResponse'; import { CollectionResponse } from "../models/response/collectionResponse";
import { FolderResponse } from '../models/response/folderResponse'; import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationCollectionResponse } from '../models/response/organizationCollectionResponse'; import { OrganizationCollectionResponse } from "../models/response/organizationCollectionResponse";
import { OrganizationResponse } from '../models/response/organizationResponse'; import { OrganizationResponse } from "../models/response/organizationResponse";
import { SendResponse } from '../models/response/sendResponse'; import { SendResponse } from "../models/response/sendResponse";
import { TemplateResponse } from '../models/response/templateResponse'; import { TemplateResponse } from "../models/response/templateResponse";
import { OrganizationCollectionRequest } from '../models/request/organizationCollectionRequest'; import { OrganizationCollectionRequest } from "../models/request/organizationCollectionRequest";
import { SelectionReadOnly } from '../models/selectionReadOnly'; import { SelectionReadOnly } from "../models/selectionReadOnly";
import { DownloadCommand } from './download.command'; import { DownloadCommand } from "./download.command";
import { CliUtils } from '../utils'; import { CliUtils } from "../utils";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class GetCommand extends DownloadCommand { export class GetCommand extends DownloadCommand {
constructor(private cipherService: CipherService, private folderService: FolderService, constructor(
private collectionService: CollectionService, private totpService: TotpService, private cipherService: CipherService,
private auditService: AuditService, cryptoService: CryptoService, private folderService: FolderService,
private userService: UserService, private searchService: SearchService, private collectionService: CollectionService,
private apiService: ApiService, private sendService: SendService, private totpService: TotpService,
private environmentService: EnvironmentService) { private auditService: AuditService,
super(cryptoService); cryptoService: CryptoService,
private userService: UserService,
private searchService: SearchService,
private apiService: ApiService,
private sendService: SendService,
private environmentService: EnvironmentService
) {
super(cryptoService);
}
async run(object: string, id: string, options: program.OptionValues): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
} }
async run(object: string, id: string, options: program.OptionValues): Promise<Response> { switch (object.toLowerCase()) {
if (id != null) { case "item":
id = id.toLowerCase(); 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, options);
case "folder":
return await this.getFolder(id);
case "collection":
return await this.getCollection(id);
case "org-collection":
return await this.getOrganizationCollection(id, options);
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.");
}
}
switch (object.toLowerCase()) { private async getCipherView(id: string): Promise<CipherView | CipherView[]> {
case 'item': let decCipher: CipherView = null;
return await this.getCipher(id); if (Utils.isGuid(id)) {
case 'username': const cipher = await this.cipherService.get(id);
return await this.getUsername(id); if (cipher != null) {
case 'password': decCipher = await cipher.decrypt();
return await this.getPassword(id); }
case 'uri': } else if (id.trim() !== "") {
return await this.getUri(id); let ciphers = await this.cipherService.getAllDecrypted();
case 'totp': ciphers = this.searchService.searchCiphersBasic(ciphers, id);
return await this.getTotp(id); if (ciphers.length > 1) {
case 'notes': return ciphers;
return await this.getNotes(id); }
case 'exposed': if (ciphers.length > 0) {
return await this.getExposed(id); decCipher = ciphers[0];
case 'attachment': }
return await this.getAttachment(id, options);
case 'folder':
return await this.getFolder(id);
case 'collection':
return await this.getCollection(id);
case 'org-collection':
return await this.getOrganizationCollection(id, options);
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[]> { return decCipher;
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;
} }
private async getCipher(id: string, filter?: (c: CipherView) => boolean) { const cipher = cipherResponse.data as CipherResponse;
let decCipher = await this.getCipherView(id); if (cipher.type !== CipherType.Login) {
if (decCipher == null) { return Response.badRequest("Not a login.");
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) { if (Utils.isNullOrWhitespace(cipher.login.username)) {
const cipherResponse = await this.getCipher(id, return Response.error("No username available for this login.");
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 res = new StringResponse(cipher.login.username);
const cipherResponse = await this.getCipher(id, return Response.success(res);
c => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.password)); }
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse; private async getPassword(id: string) {
if (cipher.type !== CipherType.Login) { const cipherResponse = await this.getCipher(
return Response.badRequest('Not a login.'); id,
} (c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.password)
);
if (Utils.isNullOrWhitespace(cipher.login.password)) { if (!cipherResponse.success) {
return Response.error('No password available for this login.'); return cipherResponse;
}
const res = new StringResponse(cipher.login.password);
return Response.success(res);
} }
private async getUri(id: string) { const cipher = cipherResponse.data as CipherResponse;
const cipherResponse = await this.getCipher(id, if (cipher.type !== CipherType.Login) {
c => c.type === CipherType.Login && c.login.uris != null && c.login.uris.length > 0 && return Response.badRequest("Not a login.");
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) { if (Utils.isNullOrWhitespace(cipher.login.password)) {
const cipherResponse = await this.getCipher(id, return Response.error("No password available for this login.");
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.userService.canAccessPremium();
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 res = new StringResponse(cipher.login.password);
const cipherResponse = await this.getCipher(id, return Response.success(res);
c => !Utils.isNullOrWhitespace(c.notes)); }
if (!cipherResponse.success) {
return cipherResponse;
}
const cipher = cipherResponse.data as CipherResponse; private async getUri(id: string) {
if (Utils.isNullOrWhitespace(cipher.notes)) { const cipherResponse = await this.getCipher(
return Response.error('No notes available for this item.'); id,
} (c) =>
c.type === CipherType.Login &&
const res = new StringResponse(cipher.notes); c.login.uris != null &&
return Response.success(res); c.login.uris.length > 0 &&
c.login.uris[0].uri !== ""
);
if (!cipherResponse.success) {
return cipherResponse;
} }
private async getExposed(id: string) { const cipher = cipherResponse.data as CipherResponse;
const passwordResponse = await this.getPassword(id); if (cipher.type !== CipherType.Login) {
if (!passwordResponse.success) { return Response.badRequest("Not a login.");
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: program.OptionValues) { if (
if (options.itemid == null || options.itemid === '') { cipher.login.uris == null ||
return Response.badRequest('--itemid <itemid> required.'); cipher.login.uris.length === 0 ||
} cipher.login.uris[0].uri === ""
) {
const itemId = options.itemid.toLowerCase(); return Response.error("No uri available for this login.");
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.userService.canAccessPremium())) {
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) { const res = new StringResponse(cipher.login.uris[0].uri);
let decFolder: FolderView = null; return Response.success(res);
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) { private async getTotp(id: string) {
return Response.notFound(); const cipherResponse = await this.getCipher(
} id,
const res = new FolderResponse(decFolder); (c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.totp)
return Response.success(res); );
if (!cipherResponse.success) {
return cipherResponse;
} }
private async getCollection(id: string) { const cipher = cipherResponse.data as CipherResponse;
let decCollection: CollectionView = null; if (cipher.type !== CipherType.Login) {
if (Utils.isGuid(id)) { return Response.badRequest("Not a login.");
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: program.OptionValues) { if (Utils.isNullOrWhitespace(cipher.login.totp)) {
if (options.organizationid == null || options.organizationid === '') { return Response.error("No TOTP available for this login.");
return Response.badRequest('--organizationid <organizationid> required.');
}
if (!Utils.isGuid(id)) {
return Response.error('`' + id + '` is not a GUID.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + 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) { const totp = await this.totpService.getCode(cipher.login.totp);
let org: Organization = null; if (totp == null) {
if (Utils.isGuid(id)) { return Response.error("Couldn't generate TOTP code.");
org = await this.userService.getOrganization(id);
} else if (id.trim() !== '') {
let orgs = await this.userService.getAllOrganizations();
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) { const canAccessPremium = await this.userService.canAccessPremium();
let template: any = null; if (!canAccessPremium) {
switch (id.toLowerCase()) { const originalCipher = await this.cipherService.get(cipher.id);
case 'item': if (
template = Cipher.template(); originalCipher == null ||
break; originalCipher.organizationId == null ||
case 'item.field': !originalCipher.organizationUseTotp
template = Field.template(); ) {
break; return Response.error("Premium status is required to use this feature.");
case 'item.login': }
template = Login.template();
break;
case 'item.login.uri':
template = LoginUri.template();
break;
case 'item.card':
template = Card.template();
break;
case 'item.identity':
template = Identity.template();
break;
case 'item.securenote':
template = SecureNote.template();
break;
case 'folder':
template = Folder.template();
break;
case 'collection':
template = Collection.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) { const res = new StringResponse(totp);
let fingerprint: string[] = null; return Response.success(res);
if (id === 'me') { }
fingerprint = await this.cryptoService.getFingerprint(await this.userService.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 { }
}
if (fingerprint == null) { private async getNotes(id: string) {
return Response.notFound(); const cipherResponse = await this.getCipher(id, (c) => !Utils.isNullOrWhitespace(c.notes));
} if (!cipherResponse.success) {
const res = new StringResponse(fingerprint.join('-')); return cipherResponse;
return Response.success(res);
} }
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: program.OptionValues) {
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.userService.canAccessPremium())) {
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: program.OptionValues) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(id)) {
return Response.error("`" + id + "` is not a GUID.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + 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.userService.getOrganization(id);
} else if (id.trim() !== "") {
let orgs = await this.userService.getAllOrganizations();
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 = Cipher.template();
break;
case "item.field":
template = Field.template();
break;
case "item.login":
template = Login.template();
break;
case "item.login.uri":
template = LoginUri.template();
break;
case "item.card":
template = Card.template();
break;
case "item.identity":
template = Identity.template();
break;
case "item.securenote":
template = SecureNote.template();
break;
case "folder":
template = Folder.template();
break;
case "collection":
template = Collection.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.userService.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 {}
}
if (fingerprint == null) {
return Response.notFound();
}
const res = new StringResponse(fingerprint.join("-"));
return Response.success(res);
}
} }

View File

@@ -1,72 +1,80 @@
import * as program from 'commander'; import * as program from "commander";
import { ImportService } from 'jslib-common/abstractions/import.service'; import { ImportService } from "jslib-common/abstractions/import.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { CliUtils } from '../utils'; import { CliUtils } from "../utils";
export class ImportCommand { export class ImportCommand {
constructor(private importService: ImportService, private userService: UserService) { } constructor(private importService: ImportService, private userService: UserService) {}
async run(format: string, filepath: string, options: program.OptionValues): Promise<Response> { async run(format: string, filepath: string, options: program.OptionValues): Promise<Response> {
const organizationId = options.organizationid; const organizationId = options.organizationid;
if (organizationId != null) { if (organizationId != null) {
const organization = await this.userService.getOrganization(organizationId); const organization = await this.userService.getOrganization(organizationId);
if (organization == null) { 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.`); 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) { if (!organization.canAccessImportExport) {
return Response.badRequest('You are not authorized to import into the provided organization.'); 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: string, filepath: string, organizationId: string) { if (options.formats || false) {
if (format == null || format === '') { return await this.list();
return Response.badRequest('`format` was not provided.'); } else {
} return await this.import(format, filepath, organizationId);
if (filepath == null || filepath === '') { }
return Response.badRequest('`filepath` was not provided.'); }
}
const importer = await this.importService.getImporter(format, organizationId); private async import(format: string, filepath: string, organizationId: string) {
if (importer === null) { if (format == null || format === "") {
return Response.badRequest('Proper importer type required.'); return Response.badRequest("`format` was not provided.");
} }
if (filepath == null || filepath === "") {
try { return Response.badRequest("`filepath` was not provided.");
const contents = await CliUtils.readFile(filepath);
if (contents === null || contents === '') {
return Response.badRequest('Import file was empty.');
}
const err = await this.importService.import(importer, contents, organizationId);
if (err != null) {
return Response.badRequest(err.message);
}
const res = new MessageResponse('Imported ' + filepath, null);
return Response.success(res);
} catch (err) {
return Response.badRequest(err);
}
} }
private async list() { const importer = await this.importService.getImporter(format, organizationId);
const options = this.importService.getImportOptions().sort((a, b) => { if (importer === null) {
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; return Response.badRequest("Proper importer type required.");
}).map(option => option.id).join('\n');
const res = new MessageResponse('Supported input formats:', options);
res.raw = options;
return Response.success(res);
} }
try {
const contents = await CliUtils.readFile(filepath);
if (contents === null || contents === "") {
return Response.badRequest("Import file was empty.");
}
const err = await this.importService.import(importer, contents, organizationId);
if (err != null) {
return Response.badRequest(err.message);
}
const res = new MessageResponse("Imported " + filepath, null);
return Response.success(res);
} 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);
}
} }

View File

@@ -1,222 +1,242 @@
import * as program from 'commander'; import * as program from "commander";
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from "jslib-common/models/view/cipherView";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { import {
CollectionDetailsResponse as ApiCollectionDetailsResponse, CollectionDetailsResponse as ApiCollectionDetailsResponse,
CollectionResponse as ApiCollectionResponse, CollectionResponse as ApiCollectionResponse,
} from 'jslib-common/models/response/collectionResponse'; } from "jslib-common/models/response/collectionResponse";
import { ListResponse as ApiListResponse } from 'jslib-common/models/response/listResponse'; import { ListResponse as ApiListResponse } from "jslib-common/models/response/listResponse";
import { CollectionData } from 'jslib-common/models/data/collectionData'; import { CollectionData } from "jslib-common/models/data/collectionData";
import { Collection } from 'jslib-common/models/domain/collection'; import { Collection } from "jslib-common/models/domain/collection";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { ListResponse } from 'jslib-node/cli/models/response/listResponse'; import { ListResponse } from "jslib-node/cli/models/response/listResponse";
import { CipherResponse } from '../models/response/cipherResponse'; import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionResponse } from '../models/response/collectionResponse'; import { CollectionResponse } from "../models/response/collectionResponse";
import { FolderResponse } from '../models/response/folderResponse'; import { FolderResponse } from "../models/response/folderResponse";
import { OrganizationResponse } from '../models/response/organizationResponse'; import { OrganizationResponse } from "../models/response/organizationResponse";
import { OrganizationUserResponse } from '../models/response/organizationUserResponse'; import { OrganizationUserResponse } from "../models/response/organizationUserResponse";
import { CliUtils } from '../utils'; import { CliUtils } from "../utils";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class ListCommand { export class ListCommand {
constructor(private cipherService: CipherService, private folderService: FolderService, constructor(
private collectionService: CollectionService, private userService: UserService, private cipherService: CipherService,
private searchService: SearchService, private apiService: ApiService) { } private folderService: FolderService,
private collectionService: CollectionService,
private userService: UserService,
private searchService: SearchService,
private apiService: ApiService
) {}
async run(object: string, cmd: program.Command): Promise<Response> { async run(object: string, cmd: program.Command): Promise<Response> {
switch (object.toLowerCase()) { switch (object.toLowerCase()) {
case 'items': case "items":
return await this.listCiphers(cmd); return await this.listCiphers(cmd);
case 'folders': case "folders":
return await this.listFolders(cmd); return await this.listFolders(cmd);
case 'collections': case "collections":
return await this.listCollections(cmd); return await this.listCollections(cmd);
case 'org-collections': case "org-collections":
return await this.listOrganizationCollections(cmd); return await this.listOrganizationCollections(cmd);
case 'org-members': case "org-members":
return await this.listOrganizationMembers(cmd); return await this.listOrganizationMembers(cmd);
case 'organizations': case "organizations":
return await this.listOrganizations(cmd); return await this.listOrganizations(cmd);
default: default:
return Response.badRequest('Unknown object.'); return Response.badRequest("Unknown object.");
} }
}
private async listCiphers(options: program.OptionValues) {
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();
} }
private async listCiphers(options: program.OptionValues) { if (
let ciphers: CipherView[]; options.folderid != null ||
options.trash = options.trash || false; options.collectionid != null ||
if (options.url != null && options.url.trim() !== '') { options.organizationid != null
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url); ) {
} else { ciphers = ciphers.filter((c) => {
ciphers = await this.cipherService.getAllDecrypted(); if (options.trash !== c.isDeleted) {
return false;
} }
if (options.folderid != null) {
if (options.folderid != null || options.collectionid != null || options.organizationid != null) { if (options.folderid === "notnull" && c.folderId != null) {
ciphers = ciphers.filter(c => { return true;
if (options.trash !== c.isDeleted) { }
return false; const folderId = options.folderid === "null" ? null : options.folderid;
} if (folderId === c.folderId) {
if (options.folderid != null) { return true;
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: program.OptionValues) {
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: program.OptionValues) {
let collections = await this.collectionService.getAllDecrypted();
if (options.organizationid != null) { if (options.organizationid != null) {
collections = collections.filter(c => { if (options.organizationid === "notnull" && c.organizationId != null) {
if (options.organizationid === c.organizationId) { return true;
return true; }
} const organizationId = options.organizationid === "null" ? null : options.organizationid;
return false; if (organizationId === c.organizationId) {
}); return true;
}
} }
if (options.search != null && options.search.trim() !== '') { if (options.collectionid != null) {
collections = CliUtils.searchCollections(collections, options.search); 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;
const res = new ListResponse(collections.map(o => new CollectionResponse(o))); });
return Response.success(res); } else if (options.search == null || options.search.trim() === "") {
ciphers = ciphers.filter((c) => options.trash === c.isDeleted);
} }
private async listOrganizationCollections(options: program.OptionValues) { if (options.search != null && options.search.trim() !== "") {
if (options.organizationid == null || options.organizationid === '') { ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash);
return Response.badRequest('--organizationid <organizationid> required.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
}
const organization = await this.userService.getOrganization(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: program.OptionValues) { const res = new ListResponse(ciphers.map((o) => new CipherResponse(o)));
if (options.organizationid == null || options.organizationid === '') { return Response.success(res);
return Response.badRequest('--organizationid <organizationid> required.'); }
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
}
const organization = await this.userService.getOrganization(options.organizationid);
if (organization == null) {
return Response.error('Organization not found.');
}
try { private async listFolders(options: program.OptionValues) {
const response = await this.apiService.getOrganizationUsers(options.organizationid); let folders = await this.folderService.getAllDecrypted();
const res = new ListResponse(response.data.map(r => {
const u = new OrganizationUserResponse(); if (options.search != null && options.search.trim() !== "") {
u.email = r.email; folders = CliUtils.searchFolders(folders, options.search);
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: program.OptionValues) { const res = new ListResponse(folders.map((o) => new FolderResponse(o)));
let organizations = await this.userService.getAllOrganizations(); return Response.success(res);
}
if (options.search != null && options.search.trim() !== '') { private async listCollections(options: program.OptionValues) {
organizations = CliUtils.searchOrganizations(organizations, options.search); let collections = await this.collectionService.getAllDecrypted();
if (options.organizationid != null) {
collections = collections.filter((c) => {
if (options.organizationid === c.organizationId) {
return true;
} }
return false;
const res = new ListResponse(organizations.map(o => new OrganizationResponse(o))); });
return Response.success(res);
} }
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: program.OptionValues) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + options.organizationid + "` is not a GUID.");
}
const organization = await this.userService.getOrganization(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: program.OptionValues) {
if (options.organizationid == null || options.organizationid === "") {
return Response.badRequest("--organizationid <organizationid> required.");
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error("`" + options.organizationid + "` is not a GUID.");
}
const organization = await this.userService.getOrganization(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: program.OptionValues) {
let organizations = await this.userService.getAllOrganizations();
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);
}
} }

View File

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

View File

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

View File

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

View File

@@ -1,114 +1,129 @@
import * as program from 'commander'; import * as program from "commander";
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; import * as path from "path";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { SendType } from 'jslib-common/enums/sendType'; import { SendType } from "jslib-common/enums/sendType";
import { NodeUtils } from 'jslib-common/misc/nodeUtils'; import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { SendResponse } from '../../models/response/sendResponse'; import { SendResponse } from "../../models/response/sendResponse";
import { SendTextResponse } from '../../models/response/sendTextResponse'; import { SendTextResponse } from "../../models/response/sendTextResponse";
import { CliUtils } from '../../utils'; import { CliUtils } from "../../utils";
export class SendCreateCommand { export class SendCreateCommand {
constructor(private sendService: SendService, private userService: UserService, constructor(
private environmentService: EnvironmentService) { } private sendService: SendService,
private userService: UserService,
private environmentService: EnvironmentService
) {}
async run(requestJson: string, options: program.OptionValues) { async run(requestJson: string, options: program.OptionValues) {
let req: any = null; let req: any = null;
if (requestJson == null || requestJson === '') { if (requestJson == null || requestJson === "") {
requestJson = await CliUtils.readStdin(); requestJson = await CliUtils.readStdin();
}
if (requestJson == null || requestJson === '') {
return Response.badRequest('`requestJson` was not provided.');
}
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);
}
return this.createSend(req, options);
} }
private async createSend(req: SendResponse, options: program.OptionValues) { if (requestJson == null || requestJson === "") {
const filePath = req.file?.fileName ?? options.file; return Response.badRequest("`requestJson` was not provided.");
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 (!(await this.userService.canAccessPremium())) {
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 encoded 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 encoded 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(options.fullObject ? res :
new StringResponse('Send created! It can be accessed at:\n' + res.accessUrl));
} catch (e) {
return Response.error(e);
}
} }
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);
}
return this.createSend(req, options);
}
private async createSend(req: SendResponse, options: program.OptionValues) {
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 (!(await this.userService.canAccessPremium())) {
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 encoded 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 encoded 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(
options.fullObject
? res
: new StringResponse("Send created! It can be accessed at:\n" + res.accessUrl)
);
} catch (e) {
return Response.error(e);
}
}
} }

View File

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

View File

@@ -1,76 +1,79 @@
import * as program from 'commander'; import * as program from "commander";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { SendType } from 'jslib-common/enums/sendType'; import { SendType } from "jslib-common/enums/sendType";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { SendResponse } from '../../models/response/sendResponse'; import { SendResponse } from "../../models/response/sendResponse";
import { CliUtils } from '../../utils'; import { CliUtils } from "../../utils";
import { SendGetCommand } from './get.command'; import { SendGetCommand } from "./get.command";
export class SendEditCommand { export class SendEditCommand {
constructor(private sendService: SendService, private userService: UserService, constructor(
private getCommand: SendGetCommand) { } private sendService: SendService,
private userService: UserService,
private getCommand: SendGetCommand
) {}
async run(encodedJson: string, options: program.OptionValues): Promise<Response> { async run(encodedJson: string, options: program.OptionValues): Promise<Response> {
if (encodedJson == null || encodedJson === '') { if (encodedJson == null || encodedJson === "") {
encodedJson = await CliUtils.readStdin(); encodedJson = await CliUtils.readStdin();
}
if (encodedJson == null || encodedJson === '') {
return Response.badRequest('`encodedJson` was not provided.');
}
let req: SendResponse = null;
try {
const reqJson = Buffer.from(encodedJson, 'base64').toString();
req = SendResponse.fromJson(reqJson);
} catch (e) {
return Response.badRequest('Error parsing the encoded request data.');
}
req.id = options.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.userService.canAccessPremium())) {
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, {});
} }
if (encodedJson == null || encodedJson === "") {
return Response.badRequest("`encodedJson` was not provided.");
}
let req: SendResponse = null;
try {
const reqJson = Buffer.from(encodedJson, "base64").toString();
req = SendResponse.fromJson(reqJson);
} catch (e) {
return Response.badRequest("Error parsing the encoded request data.");
}
req.id = options.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.userService.canAccessPremium())) {
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, {});
}
} }

View File

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

View File

@@ -1,28 +1,30 @@
import * as program from 'commander'; import * as program from "commander";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from 'jslib-common/abstractions/send.service'; import { SendService } from "jslib-common/abstractions/send.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { ListResponse } from 'jslib-node/cli/models/response/listResponse'; import { ListResponse } from "jslib-node/cli/models/response/listResponse";
import { SendResponse } from '../..//models/response/sendResponse'; import { SendResponse } from "../..//models/response/sendResponse";
export class SendListCommand { export class SendListCommand {
constructor(
private sendService: SendService,
private environmentService: EnvironmentService,
private searchService: SearchService
) {}
constructor(private sendService: SendService, private environmentService: EnvironmentService, async run(options: program.OptionValues): Promise<Response> {
private searchService: SearchService) { } let sends = await this.sendService.getAllDecrypted();
async run(options: program.OptionValues): Promise<Response> { if (options.search != null && options.search.trim() !== "") {
let sends = await this.sendService.getAllDecrypted(); sends = this.searchService.searchSends(sends, options.search);
if (options.search != null && options.search.trim() !== '') {
sends = this.searchService.searchSends(sends, options.search);
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
const res = new ListResponse(sends.map(s => new SendResponse(s, webVaultUrl)));
return Response.success(res);
} }
const webVaultUrl = this.environmentService.getWebVaultUrl();
const res = new ListResponse(sends.map((s) => new SendResponse(s, webVaultUrl)));
return Response.success(res);
}
} }

View File

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

View File

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

View File

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

View File

@@ -1,50 +1,55 @@
import * as program from 'commander'; import * as program from "commander";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { TemplateResponse } from '../models/response/templateResponse'; import { TemplateResponse } from "../models/response/templateResponse";
export class StatusCommand { export class StatusCommand {
constructor(private envService: EnvironmentService, private syncService: SyncService, constructor(
private userService: UserService, private vaultTimeoutService: VaultTimeoutService) { private envService: EnvironmentService,
private syncService: SyncService,
private userService: UserService,
private vaultTimeoutService: VaultTimeoutService
) {}
async run(): Promise<Response> {
try {
const baseUrl = this.baseUrl();
const status = await this.status();
const lastSync = await this.syncService.getLastSync();
const userId = await this.userService.getUserId();
const email = await this.userService.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<string> {
const authed = await this.userService.isAuthenticated();
if (!authed) {
return "unauthenticated";
} }
async run(): Promise<Response> { const isLocked = await this.vaultTimeoutService.isLocked();
try { return isLocked ? "locked" : "unlocked";
const baseUrl = this.baseUrl(); }
const status = await this.status();
const lastSync = await this.syncService.getLastSync();
const userId = await this.userService.getUserId();
const email = await this.userService.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<string> {
const authed = await this.userService.isAuthenticated();
if (!authed) {
return 'unauthenticated';
}
const isLocked = await this.vaultTimeoutService.isLocked();
return isLocked ? 'locked' : 'unlocked';
}
} }

View File

@@ -1,31 +1,31 @@
import * as program from 'commander'; import * as program from "commander";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse'; import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
export class SyncCommand { export class SyncCommand {
constructor(private syncService: SyncService) { } constructor(private syncService: SyncService) {}
async run(options: program.OptionValues): Promise<Response> { async run(options: program.OptionValues): Promise<Response> {
if (options.last || false) { if (options.last || false) {
return await this.getLastSync(); return await this.getLastSync();
}
try {
const result = await this.syncService.fullSync(options.force || false, true);
const res = new MessageResponse('Syncing complete.', null);
return Response.success(res);
} catch (e) {
return Response.error('Syncing failed: ' + e.toString());
}
} }
private async getLastSync() { try {
const lastSyncDate = await this.syncService.getLastSync(); const result = await this.syncService.fullSync(options.force || false, true);
const res = new StringResponse(lastSyncDate == null ? null : lastSyncDate.toISOString()); const res = new MessageResponse("Syncing complete.", null);
return Response.success(res); 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);
}
} }

View File

@@ -1,98 +1,120 @@
import * as program from 'commander'; import * as program from "commander";
import * as inquirer from 'inquirer'; import * as inquirer from "inquirer";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { UserService } from 'jslib-common/abstractions/user.service'; import { UserService } from "jslib-common/abstractions/user.service";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest'; import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { HashPurpose } from 'jslib-common/enums/hashPurpose'; import { HashPurpose } from "jslib-common/enums/hashPurpose";
import { NodeUtils } from 'jslib-common/misc/nodeUtils'; import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { ConsoleLogService } from 'jslib-common/services/consoleLog.service'; import { ConsoleLogService } from "jslib-common/services/consoleLog.service";
export class UnlockCommand { export class UnlockCommand {
constructor(private cryptoService: CryptoService, private userService: UserService, constructor(
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService, private cryptoService: CryptoService,
private logService: ConsoleLogService) { private userService: UserService,
} private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private logService: ConsoleLogService
) {}
async run(password: string, options: program.OptionValues) { async run(password: string, options: program.OptionValues) {
const canInteract = process.env.BW_NOINTERACTION !== 'true'; const canInteract = process.env.BW_NOINTERACTION !== "true";
if (password == null || password === '') { if (password == null || password === "") {
if (options?.passwordfile) { if (options?.passwordfile) {
password = await NodeUtils.readFirstLine(options.passwordfile); password = await NodeUtils.readFirstLine(options.passwordfile);
} else if (options?.passwordenv) { } else if (options?.passwordenv) {
if (process.env[options.passwordenv]) { if (process.env[options.passwordenv]) {
password = process.env[options.passwordenv]; password = process.env[options.passwordenv];
} else {
this.logService.warning(`Warning: Provided passwordenv ${options.passwordenv} is not set`);
}
}
}
if (password == null || password === '') {
if (canInteract) {
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.');
}
}
this.setNewSessionKey();
const email = await this.userService.getEmail();
const kdf = await this.userService.getKdf();
const kdfIterations = await this.userService.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 { }
}
}
if (passwordValid) {
await this.cryptoService.setKey(key);
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);
} else { } else {
return Response.error('Invalid master password.'); this.logService.warning(
`Warning: Provided passwordenv ${options.passwordenv} is not set`
);
} }
}
} }
private async setNewSessionKey() { if (password == null || password === "") {
const key = await this.cryptoFunctionService.randomBytes(64); if (canInteract) {
process.env.BW_SESSION = Utils.fromBufferToB64(key); 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.");
}
} }
this.setNewSessionKey();
const email = await this.userService.getEmail();
const kdf = await this.userService.getKdf();
const kdfIterations = await this.userService.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 {}
}
}
if (passwordValid) {
await this.cryptoService.setKey(key);
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);
} else {
return Response.error("Invalid master password.");
}
}
private async setNewSessionKey() {
const key = await this.cryptoFunctionService.randomBytes(64);
process.env.BW_SESSION = Utils.fromBufferToB64(key);
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,450 +1,508 @@
import * as chalk from 'chalk'; import * as chalk from "chalk";
import * as program from 'commander'; import * as program from "commander";
import { Main } from './bw'; import { Main } from "./bw";
import { ConfigCommand } from './commands/config.command'; import { ConfigCommand } from "./commands/config.command";
import { EncodeCommand } from './commands/encode.command'; import { EncodeCommand } from "./commands/encode.command";
import { GenerateCommand } from './commands/generate.command'; import { GenerateCommand } from "./commands/generate.command";
import { LockCommand } from './commands/lock.command'; import { LockCommand } from "./commands/lock.command";
import { LoginCommand } from './commands/login.command'; import { LoginCommand } from "./commands/login.command";
import { StatusCommand } from './commands/status.command'; import { StatusCommand } from "./commands/status.command";
import { SyncCommand } from './commands/sync.command'; import { SyncCommand } from "./commands/sync.command";
import { UnlockCommand } from './commands/unlock.command'; import { UnlockCommand } from "./commands/unlock.command";
import { CompletionCommand } from './commands/completion.command'; import { CompletionCommand } from "./commands/completion.command";
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command'; import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
import { UpdateCommand } from 'jslib-node/cli/commands/update.command'; import { UpdateCommand } from "jslib-node/cli/commands/update.command";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { TemplateResponse } from './models/response/templateResponse'; import { TemplateResponse } from "./models/response/templateResponse";
import { CliUtils } from './utils'; import { CliUtils } from "./utils";
import { BaseProgram } from 'jslib-node/cli/baseProgram'; import { BaseProgram } from "jslib-node/cli/baseProgram";
const writeLn = CliUtils.writeLn; const writeLn = CliUtils.writeLn;
export class Program extends BaseProgram { export class Program extends BaseProgram {
constructor(protected main: Main) { constructor(protected main: Main) {
super(main.userService, writeLn); super(main.userService, writeLn);
} }
async register() { async register() {
program program
.option('--pretty', 'Format output. JSON is tabbed with two spaces.') .option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option('--raw', 'Return raw output instead of a descriptive message.') .option("--raw", "Return raw output instead of a descriptive message.")
.option('--response', 'Return a JSON formatted version of response output.') .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("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
.option('--quiet', 'Don\'t return anything to stdout.') .option("--quiet", "Don't return anything to stdout.")
.option('--nointeraction', 'Do not prompt for interactive user input.') .option("--nointeraction", "Do not prompt for interactive user input.")
.option('--session <session>', 'Pass session key instead of reading from env.') .option("--session <session>", "Pass session key instead of reading from env.")
.version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version'); .version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
program.on('option:pretty', () => { program.on("option:pretty", () => {
process.env.BW_PRETTY = 'true'; process.env.BW_PRETTY = "true";
}); });
program.on('option:raw', () => { program.on("option:raw", () => {
process.env.BW_RAW = 'true'; process.env.BW_RAW = "true";
}); });
program.on('option:quiet', () => { program.on("option:quiet", () => {
process.env.BW_QUIET = 'true'; process.env.BW_QUIET = "true";
}); });
program.on('option:response', () => { program.on("option:response", () => {
process.env.BW_RESPONSE = 'true'; process.env.BW_RESPONSE = "true";
}); });
program.on('option:cleanexit', () => { program.on("option:cleanexit", () => {
process.env.BW_CLEANEXIT = 'true'; process.env.BW_CLEANEXIT = "true";
}); });
program.on('option:nointeraction', () => { program.on("option:nointeraction", () => {
process.env.BW_NOINTERACTION = 'true'; process.env.BW_NOINTERACTION = "true";
}); });
program.on('option:session', key => { program.on("option:session", (key) => {
process.env.BW_SESSION = key; process.env.BW_SESSION = key;
}); });
program.on('command:*', () => { program.on("command:*", () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true); writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
writeLn('See --help for a list of available commands.', true, true); writeLn("See --help for a list of available commands.", true, true);
process.exitCode = 1; process.exitCode = 1;
}); });
program.on('--help', () => { program.on("--help", () => {
writeLn('\n Examples:'); writeLn("\n Examples:");
writeLn(''); writeLn("");
writeLn(' bw login'); writeLn(" bw login");
writeLn(' bw lock'); writeLn(" bw lock");
writeLn(' bw unlock myPassword321'); writeLn(" bw unlock myPassword321");
writeLn(' bw list --help'); writeLn(" bw list --help");
writeLn(' bw list items --search google'); writeLn(" bw list items --search google");
writeLn(' bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412'); writeLn(" bw get item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(' bw get password google.com'); writeLn(" bw get password google.com");
writeLn(' echo \'{"name":"My Folder"}\' | bw encode'); writeLn(' echo \'{"name":"My Folder"}\' | bw encode');
writeLn(' bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K'); writeLn(" bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K");
writeLn(' bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=='); writeLn(
writeLn(' bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412'); " bw edit folder c7c7b60b-9c61-40f2-8ccd-36c49595ed72 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=="
writeLn(' bw generate -lusn --length 18'); );
writeLn(' bw config server https://bitwarden.example.com'); writeLn(" bw delete item 99ee88d2-6046-4ea7-92c2-acac464b1412");
writeLn(' bw send -f ./file.ext'); writeLn(" bw generate -lusn --length 18");
writeLn(' bw send "text to send"'); writeLn(" bw config server https://bitwarden.example.com");
writeLn(' echo "text to send" | bw send'); writeLn(" bw send -f ./file.ext");
writeLn(' bw receive https://vault.bitwarden.com/#/send/rg3iuoS_Akm2gqy6ADRHmg/Ht7dYjsqjmgqUM3rjzZDSQ'); writeLn(' bw send "text to send"');
writeLn('', true); writeLn(' echo "text to send" | bw send');
}); writeLn(
" bw receive https://vault.bitwarden.com/#/send/rg3iuoS_Akm2gqy6ADRHmg/Ht7dYjsqjmgqUM3rjzZDSQ"
);
writeLn("", true);
});
program program
.command('login [email] [password]') .command("login [email] [password]")
.description('Log into a user account.') .description("Log into a user account.")
.option('--method <method>', 'Two-step login method.') .option("--method <method>", "Two-step login method.")
.option('--code <code>', 'Two-step login code.') .option("--code <code>", "Two-step login code.")
.option('--sso', 'Log in with Single-Sign On.') .option("--sso", "Log in with Single-Sign On.")
.option('--apikey', 'Log in with an Api Key.') .option("--apikey", "Log in with an Api Key.")
.option('--passwordenv <passwordenv>', 'Environment variable storing your password') .option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option('--passwordfile <passwordfile>', 'Path to a file containing your password as its first line') .option(
.option('--check', 'Check login status.', async () => { "--passwordfile <passwordfile>",
const authed = await this.main.userService.isAuthenticated(); "Path to a file containing your password as its first line"
if (authed) { )
const res = new MessageResponse('You are logged in!', null); .option("--check", "Check login status.", async () => {
this.processResponse(Response.success(res), true); const authed = await this.main.userService.isAuthenticated();
} if (authed) {
this.processResponse(Response.error('You are not logged in.'), true); const res = new MessageResponse("You are logged in!", null);
}) this.processResponse(Response.success(res), 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.syncService, this.main.i18nService,
this.main.environmentService, this.main.passwordGenerationService,
this.main.platformUtilsService, this.main.userService, this.main.cryptoService,
this.main.policyService, 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 (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(cmd);
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 () => {
const locked = await this.main.vaultTimeoutService.isLocked();
if (!locked) {
const res = new MessageResponse('Vault is unlocked!', null);
this.processResponse(Response.success(res), true);
}
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.userService,
this.main.cryptoFunctionService, this.main.apiService, this.main.logService);
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);
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.userService,
this.main.vaultTimeoutService);
const response = await command.run();
this.processResponse(response);
});
}
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();
const hasKey = await this.main.cryptoService.hasKey();
if (!hasKey) {
const canInteract = process.env.BW_NOINTERACTION !== 'true';
if (canInteract) {
const usesKeyConnector = await this.main.keyConnectorService.getUsesKeyConnector();
if (usesKeyConnector) {
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.userService,
this.main.cryptoFunctionService, this.main.apiService, this.main.logService);
const response = await command.run(null, null);
if (!response.success) {
this.processResponse(response, true);
}
}
} else {
this.processResponse(Response.error('Vault is locked.'), true);
}
} else if (!this.main.cryptoService.hasKeyInMemory()) {
await this.main.cryptoService.getKey();
} }
} 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.syncService,
this.main.i18nService,
this.main.environmentService,
this.main.passwordGenerationService,
this.main.platformUtilsService,
this.main.userService,
this.main.cryptoService,
this.main.policyService,
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 (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(cmd);
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 () => {
const locked = await this.main.vaultTimeoutService.isLocked();
if (!locked) {
const res = new MessageResponse("Vault is unlocked!", null);
this.processResponse(Response.success(res), true);
}
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.userService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService
);
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);
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.userService,
this.main.vaultTimeoutService
);
const response = await command.run();
this.processResponse(response);
});
}
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();
const hasKey = await this.main.cryptoService.hasKey();
if (!hasKey) {
const canInteract = process.env.BW_NOINTERACTION !== "true";
if (canInteract) {
const usesKeyConnector = await this.main.keyConnectorService.getUsesKeyConnector();
if (usesKeyConnector) {
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.userService,
this.main.cryptoFunctionService,
this.main.apiService,
this.main.logService
);
const response = await command.run(null, null);
if (!response.success) {
this.processResponse(response, true);
}
}
} else {
this.processResponse(Response.error("Vault is locked."), true);
}
} else if (!this.main.cryptoService.hasKeyInMemory()) {
await this.main.cryptoService.getKey();
}
}
} }

View File

@@ -1,280 +1,338 @@
import * as chalk from 'chalk'; import * as chalk from "chalk";
import * as program from 'commander'; import * as program from "commander";
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; import * as path from "path";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { SendType } from 'jslib-common/enums/sendType'; import { SendType } from "jslib-common/enums/sendType";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { GetCommand } from './commands/get.command'; import { GetCommand } from "./commands/get.command";
import { SendCreateCommand } from './commands/send/create.command'; import { SendCreateCommand } from "./commands/send/create.command";
import { SendDeleteCommand } from './commands/send/delete.command'; import { SendDeleteCommand } from "./commands/send/delete.command";
import { SendEditCommand } from './commands/send/edit.command'; import { SendEditCommand } from "./commands/send/edit.command";
import { SendGetCommand } from './commands/send/get.command'; import { SendGetCommand } from "./commands/send/get.command";
import { SendListCommand } from './commands/send/list.command'; import { SendListCommand } from "./commands/send/list.command";
import { SendReceiveCommand } from './commands/send/receive.command'; import { SendReceiveCommand } from "./commands/send/receive.command";
import { SendRemovePasswordCommand } from './commands/send/removePassword.command'; import { SendRemovePasswordCommand } from "./commands/send/removePassword.command";
import { SendFileResponse } from './models/response/sendFileResponse'; import { SendFileResponse } from "./models/response/sendFileResponse";
import { SendResponse } from './models/response/sendResponse'; import { SendResponse } from "./models/response/sendResponse";
import { SendTextResponse } from './models/response/sendTextResponse'; import { SendTextResponse } from "./models/response/sendTextResponse";
import { Main } from './bw'; import { Main } from "./bw";
import { Program } from './program'; import { Program } from "./program";
import { CliUtils } from './utils'; import { CliUtils } from "./utils";
const writeLn = CliUtils.writeLn; const writeLn = CliUtils.writeLn;
export class SendProgram extends Program { export class SendProgram extends Program {
constructor(main: Main) { constructor(main: Main) {
super(main); super(main);
} }
async register() { async register() {
program.addCommand(this.sendCommand()); program.addCommand(this.sendCommand());
// receive is accessible both at `bw receive` and `bw send receive` // receive is accessible both at `bw receive` and `bw send receive`
program.addCommand(this.receiveCommand()); program.addCommand(this.receiveCommand());
} }
private sendCommand(): program.Command { private sendCommand(): program.Command {
return new program.Command('send') return new program.Command("send")
.arguments('<data>') .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', { .description(
data: 'The data to Send. Specify as a filepath with the --file option', "Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send",
}) {
.option('-f, --file', 'Specifies that <data> is a filepath') data: "The data to Send. Specify as a filepath with the --file option",
.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("-f, --file", "Specifies that <data> is a filepath")
.option('-n, --name <name>', 'The name of the Send. Defaults to a guid for text Sends and the filename for files.') .option(
.option('--notes <notes>', 'Notes to add to the Send.') "-d, --deleteInDays <days>",
.option('--fullObject', 'Specifies that the full Send object should be returned rather than just the access url.') "The number of days in the future to set deletion date, defaults to 7",
.addCommand(this.listCommand()) "7"
.addCommand(this.templateCommand()) )
.addCommand(this.getCommand()) .option("-a, --maxAccessCount <amount>", "The amount of max possible accesses.")
.addCommand(this.receiveCommand()) .option("--hidden", "Hide <data> in web by default. Valid only if --file is not set.")
.addCommand(this.createCommand()) .option(
.addCommand(this.editCommand()) "-n, --name <name>",
.addCommand(this.removePasswordCommand()) "The name of the Send. Defaults to a guid for text Sends and the filename for files."
.addCommand(this.deleteCommand()) )
.action(async (data: string, options: program.OptionValues) => { .option("--notes <notes>", "Notes to add to the Send.")
const encodedJson = this.makeSendJson(data, options); .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; let response: Response;
if (encodedJson instanceof Response) { if (encodedJson instanceof Response) {
response = encodedJson; 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 Send\s 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.userService, this.main.searchService, this.main.apiService, this.main.sendService,
this.main.environmentService);
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.userService, 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 { } else {
sendText = SendTextResponse.template(data, options.hidden); response = await this.runCreate(encodedJson, options);
} }
const template = Utils.assign(SendResponse.template(null, options.deleteInDays), { this.processResponse(response);
name: options.name ?? name, });
notes: options.notes, }
file: sendFile,
text: sendText,
type: type,
});
return Buffer.from(JSON.stringify(template), 'utf8').toString('base64'); 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 async runCreate(encodedJson: string, options: program.OptionValues) { 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(); await this.exitIfLocked();
const cmd = new SendCreateCommand(this.main.sendService, this.main.userService, const cmd = new SendListCommand(
this.main.environmentService); this.main.sendService,
return await cmd.run(encodedJson, options); 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.userService,
this.main.searchService,
this.main.apiService,
this.main.sendService,
this.main.environmentService
);
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.userService, 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.userService,
this.main.environmentService
);
return await cmd.run(encodedJson, options);
}
} }

View File

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

View File

@@ -1,93 +1,100 @@
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
export class NodeEnvSecureStorageService implements StorageService { export class NodeEnvSecureStorageService implements StorageService {
constructor(private storageService: StorageService, private logService: LogService, constructor(
private cryptoService: () => CryptoService) { } private storageService: StorageService,
private logService: LogService,
private cryptoService: () => CryptoService
) {}
async get<T>(key: string): Promise<T> { async get<T>(key: string): Promise<T> {
const value = await this.storageService.get<string>(this.makeProtectedStorageKey(key)); const value = await this.storageService.get<string>(this.makeProtectedStorageKey(key));
if (value == null) { if (value == null) {
return null; return null;
} }
const obj = await this.decrypt(value); const obj = await this.decrypt(value);
return obj as any; 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 (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.");
} }
async has(key: string): Promise<boolean> { return Utils.fromBufferToB64(encValue.buffer);
return (await this.get(key)) != null; }
}
async save(key: string, obj: any): Promise<any> {
if (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.');
}
private async decrypt(encValue: string): Promise<string> {
try {
const sessionKey = this.getSessionKey();
if (sessionKey == null) {
return 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.");
} }
private makeProtectedStorageKey(key: string) { return null;
return '__PROTECTED__' + key; }
}
private makeProtectedStorageKey(key: string) {
return "__PROTECTED__" + key;
}
} }

View File

@@ -1,173 +1,173 @@
import * as fs from 'fs'; import * as fs from "fs";
import * as path from 'path'; import * as path from "path";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse'; import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { Organization } from 'jslib-common/models/domain/organization'; import { Organization } from "jslib-common/models/domain/organization";
import { CollectionView } from 'jslib-common/models/view/collectionView'; import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from 'jslib-common/models/view/folderView'; import { FolderView } from "jslib-common/models/view/folderView";
import { NodeUtils } from 'jslib-common/misc/nodeUtils'; import { NodeUtils } from "jslib-common/misc/nodeUtils";
export class CliUtils { export class CliUtils {
static writeLn(s: string, finalLine: boolean = false, error: boolean = false) { static writeLn(s: string, finalLine: boolean = false, error: boolean = false) {
const stream = error ? process.stderr : process.stdout; const stream = error ? process.stderr : process.stdout;
if (finalLine && (process.platform === 'win32' || !stream.isTTY)) { if (finalLine && (process.platform === "win32" || !stream.isTTY)) {
stream.write(s); 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 { } else {
stream.write(s + '\n'); 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 readFile(input: string): Promise<string> { /**
return new Promise<string>((resolve, reject) => { * Save the given data to a file and determine the target file if necessary.
let p: string = null; * If output is non-empty, it is used as target filename. Otherwise the target filename is
if (input != null && input !== '') { * built from the current working directory and the given defaultFileName.
const osInput = path.join(input); *
if (osInput.indexOf(path.sep) === -1) { * @param data to be written to the file.
p = path.join(process.cwd(), osInput); * @param output file to write to or empty to choose automatically.
} else { * @param defaultFileName to use when no explicit output filename is given.
p = osInput; * @return the chosen output file.
} */
} else { static saveFile(data: string | Buffer, output: string, defaultFileName: string) {
reject('You must specify a file path.'); let p: string = null;
} let mkdir = false;
fs.readFile(p, 'utf8', (err, data) => { if (output != null && output !== "") {
if (err != null) { const osOutput = path.join(output);
reject(err.message); if (osOutput.indexOf(path.sep) === -1) {
} p = path.join(process.cwd(), osOutput);
resolve(data); } else {
}); mkdir = true;
}); if (osOutput.endsWith(path.sep)) {
} p = path.join(osOutput, defaultFileName);
/**
* 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 { } else {
p = path.join(process.cwd(), defaultFileName); p = osOutput;
} }
}
} else {
p = path.join(process.cwd(), defaultFileName);
}
p = path.resolve(p); p = path.resolve(p);
if (mkdir) { if (mkdir) {
const dir = p.substring(0, p.lastIndexOf(path.sep)); const dir = p.substring(0, p.lastIndexOf(path.sep));
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
NodeUtils.mkdirpSync(dir, '700'); 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);
});
});
}
return new Promise<string>((resolve, reject) => { /**
fs.writeFile(p, data, { encoding: 'utf8', mode: 0o600 }, err => { * Process the given data and write it to a file if possible. If the user requested RAW output and
if (err != null) { * no output name is given, the file is directly written to stdout. The resulting Response contains
reject('Cannot save file to ' + p); * an otherwise empty message then to prevent writing other information to stdout.
} *
resolve(p); * 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);
* Process the given data and write it to a file if possible. If the user requested RAW output and const res = new MessageResponse("Saved " + filePath, null);
* no output name is given, the file is directly written to stdout. The resulting Response contains res.raw = filePath;
* an otherwise empty message then to prevent writing other information to stdout. return Response.success(res);
* }
* If an output is given or no RAW output is requested, the rules from [saveFile] apply.
* static readStdin(): Promise<string> {
* @param data to be written to the file or stdout. return new Promise((resolve, reject) => {
* @param output file to write to or empty to choose automatically. let input: string = "";
* @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. if (process.stdin.isTTY) {
*/ resolve(input);
static async saveResultToFile(data: string | Buffer, output: string, defaultFileName: string) { return;
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.stdin.setEncoding("utf8");
process.stdout.write(data); process.stdin.on("readable", () => {
return Response.success(); while (true) {
const chunk = process.stdin.read();
if (chunk == null) {
break;
}
input += chunk;
} }
});
const filePath = await this.saveFile(data, output, defaultFileName); process.stdin.on("end", () => {
const res = new MessageResponse('Saved ' + filePath, null); resolve(input);
res.raw = filePath; });
return Response.success(res); });
} }
static readStdin(): Promise<string> { static searchFolders(folders: FolderView[], search: string) {
return new Promise((resolve, reject) => { search = search.toLowerCase();
let input: string = ''; return folders.filter((f) => {
if (f.name != null && f.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
if (process.stdin.isTTY) { static searchCollections(collections: CollectionView[], search: string) {
resolve(input); search = search.toLowerCase();
return; return collections.filter((c) => {
} if (c.name != null && c.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
process.stdin.setEncoding('utf8'); static searchOrganizations(organizations: Organization[], search: string) {
process.stdin.on('readable', () => { search = search.toLowerCase();
while (true) { return organizations.filter((o) => {
const chunk = process.stdin.read(); if (o.name != null && o.name.toLowerCase().indexOf(search) > -1) {
if (chunk == null) { return true;
break; }
} return false;
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;
});
}
} }

View File

@@ -1,433 +1,493 @@
import * as chalk from 'chalk'; import * as chalk from "chalk";
import * as program from 'commander'; import * as program from "commander";
import { Response } from 'jslib-node/cli/models/response'; import { Response } from "jslib-node/cli/models/response";
import { Main } from './bw'; import { Main } from "./bw";
import { ConfirmCommand } from './commands/confirm.command'; import { ConfirmCommand } from "./commands/confirm.command";
import { CreateCommand } from './commands/create.command'; import { CreateCommand } from "./commands/create.command";
import { DeleteCommand } from './commands/delete.command'; import { DeleteCommand } from "./commands/delete.command";
import { EditCommand } from './commands/edit.command'; import { EditCommand } from "./commands/edit.command";
import { ExportCommand } from './commands/export.command'; import { ExportCommand } from "./commands/export.command";
import { GetCommand } from './commands/get.command'; import { GetCommand } from "./commands/get.command";
import { ImportCommand } from './commands/import.command'; import { ImportCommand } from "./commands/import.command";
import { ListCommand } from './commands/list.command'; import { ListCommand } from "./commands/list.command";
import { RestoreCommand } from './commands/restore.command'; import { RestoreCommand } from "./commands/restore.command";
import { ShareCommand } from './commands/share.command'; import { ShareCommand } from "./commands/share.command";
import { CliUtils } from './utils'; import { CliUtils } from "./utils";
import { Program } from './program'; import { Program } from "./program";
const writeLn = CliUtils.writeLn; const writeLn = CliUtils.writeLn;
export class VaultProgram extends Program { export class VaultProgram extends Program {
constructor(protected main: Main) { constructor(protected main: Main) {
super(main); super(main);
} }
async register() { async register() {
program program
.addCommand(this.listCommand()) .addCommand(this.listCommand())
.addCommand(this.getCommand()) .addCommand(this.getCommand())
.addCommand(this.createCommand()) .addCommand(this.createCommand())
.addCommand(this.editCommand()) .addCommand(this.editCommand())
.addCommand(this.deleteCommand()) .addCommand(this.deleteCommand())
.addCommand(this.restoreCommand()) .addCommand(this.restoreCommand())
.addCommand(this.shareCommand('move', false)) .addCommand(this.shareCommand("move", false))
.addCommand(this.confirmCommand()) .addCommand(this.confirmCommand())
.addCommand(this.importCommand()) .addCommand(this.importCommand())
.addCommand(this.exportCommand()) .addCommand(this.exportCommand())
.addCommand(this.shareCommand('share', true)); .addCommand(this.shareCommand("share", true));
} }
private validateObject(requestedObject: string, validObjects: string[]): boolean { private validateObject(requestedObject: string, validObjects: string[]): boolean {
let success = true; let success = true;
if (!validObjects.includes(requestedObject)) { if (!validObjects.includes(requestedObject)) {
success = false; success = false;
this.processResponse(Response.badRequest('Unknown object "' + requestedObject + '". Allowed objects are ' + this.processResponse(
validObjects.join(', ') + '.')); 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;
} }
return success;
}
private listCommand(): program.Command { await this.exitIfLocked();
const listObjects = [ const command = new ListCommand(
'items', this.main.cipherService,
'folders', this.main.folderService,
'collections', this.main.collectionService,
'org-collections', this.main.userService,
'org-members', this.main.searchService,
'organizations', this.main.apiService
]; );
const response = await command.run(object, cmd);
return new program.Command('list') this.processResponse(response);
.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(); private getCommand(): program.Command {
const command = new ListCommand(this.main.cipherService, this.main.folderService, const getObjects = [
this.main.collectionService, this.main.userService, this.main.searchService, this.main.apiService); "item",
const response = await command.run(object, cmd); "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;
}
this.processResponse(response); 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.userService,
this.main.searchService,
this.main.apiService,
this.main.sendService,
this.main.environmentService
);
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private getCommand(): program.Command { private createCommand() {
const getObjects = [ const createObjects = ["item", "attachment", "folder", "org-collection"];
'item', return new program.Command("create")
'username', .arguments("<object> [encodedJson]")
'password', .description("Create an object in the vault.", {
'uri', object: "Valid objects are: " + createObjects.join(", "),
'totp', encodedJson: "Encoded json of the object to create. Can also be piped into stdin.",
'notes', })
'exposed', .option("--file <file>", "Path to file for attachment.")
'attachment', .option("--itemid <itemid>", "Attachment's item id.")
'folder', .option("--organizationid <organizationid>", "Organization id for an organization object.")
'collection', .on("--help", () => {
'org-collection', writeLn("\n Examples:");
'organization', writeLn("");
'template', writeLn(" bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K");
'fingerprint', writeLn(" echo 'eyJuYW1lIjoiTXkgRm9sZGVyIn0K' | bw create folder");
'send', writeLn(
]; " bw create attachment --file ./myfile.csv " +
return new program.Command('get') "--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66"
.arguments('<object> <id>') );
.description('Get an object from the vault.', { writeLn("", true);
object: 'Valid objects are: ' + getObjects.join(', '), })
id: 'Search term or object\'s globally unique `id`.', .action(async (object, encodedJson, cmd) => {
}) if (!this.validateObject(object, createObjects)) {
.option('--itemid <itemid>', 'Attachment\'s item id.') return;
.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(); await this.exitIfLocked();
const command = new GetCommand(this.main.cipherService, this.main.folderService, const command = new CreateCommand(
this.main.collectionService, this.main.totpService, this.main.auditService, this.main.cipherService,
this.main.cryptoService, this.main.userService, this.main.searchService, this.main.folderService,
this.main.apiService, this.main.sendService, this.main.environmentService); this.main.userService,
const response = await command.run(object, id, cmd); this.main.cryptoService,
this.processResponse(response); this.main.apiService
}); );
} const response = await command.run(object, encodedJson, cmd);
this.processResponse(response);
});
}
private createCommand() { private editCommand(): program.Command {
const createObjects = [ const editObjects = ["item", "item-collections", "folder", "org-collection"];
'item', return new program.Command("edit")
'attachment', .arguments("<object> <id> [encodedJson]")
'folder', .description("Edit an object from the vault.", {
'org-collection', object: "Valid objects are: " + editObjects.join(", "),
]; id: "Object's globally unique `id`.",
return new program.Command('create') encodedJson: "Encoded json of the object to create. Can also be piped into stdin.",
.arguments('<object> [encodedJson]') })
.description('Create an object in the vault.', { .option("--organizationid <organizationid>", "Organization id for an organization object.")
object: 'Valid objects are: ' + createObjects.join(', '), .on("--help", () => {
encodedJson: 'Encoded json of the object to create. Can also be piped into stdin.', writeLn("\n Examples:");
}) writeLn("");
.option('--file <file>', 'Path to file for attachment.') writeLn(
.option('--itemid <itemid>', 'Attachment\'s item id.') " bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=="
.option('--organizationid <organizationid>', 'Organization id for an organization object.') );
.on('--help', () => { writeLn(
writeLn('\n Examples:'); " echo 'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==' | " +
writeLn(''); "bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02"
writeLn(' bw create folder eyJuYW1lIjoiTXkgRm9sZGVyIn0K'); );
writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyIn0K\' | bw create folder'); writeLn(
writeLn(' bw create attachment --file ./myfile.csv ' + " bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 " +
'--itemid 16b15b89-65b3-4639-ad2a-95052a6d8f66'); "WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=="
writeLn('', true); );
}) writeLn("", true);
.action(async (object, encodedJson, cmd) => { })
if (!this.validateObject(object, createObjects)) { .action(async (object, id, encodedJson, cmd) => {
return; if (!this.validateObject(object, editObjects)) {
} return;
}
await this.exitIfLocked(); await this.exitIfLocked();
const command = new CreateCommand(this.main.cipherService, this.main.folderService, const command = new EditCommand(
this.main.userService, this.main.cryptoService, this.main.apiService); this.main.cipherService,
const response = await command.run(object, encodedJson, cmd); this.main.folderService,
this.processResponse(response); this.main.cryptoService,
}); this.main.apiService
} );
const response = await command.run(object, id, encodedJson, cmd);
this.processResponse(response);
});
}
private editCommand(): program.Command { private deleteCommand(): program.Command {
const editObjects = [ const deleteObjects = ["item", "attachment", "folder", "org-collection"];
'item', return new program.Command("delete")
'item-collections', .arguments("<object> <id>")
'folder', .description("Delete an object from the vault.", {
'org-collection', object: "Valid objects are: " + deleteObjects.join(", "),
]; id: "Object's globally unique `id`.",
return new program.Command('edit') })
.arguments('<object> <id> [encodedJson]') .option("--itemid <itemid>", "Attachment's item id.")
.description('Edit an object from the vault.', { .option("--organizationid <organizationid>", "Organization id for an organization object.")
object: 'Valid objects are: ' + editObjects.join(', '), .option(
id: 'Object\'s globally unique `id`.', "-p, --permanent",
encodedJson: 'Encoded json of the object to create. Can also be piped into stdin.', "Permanently deletes the item instead of soft-deleting it (item only)."
}) )
.option('--organizationid <organizationid>', 'Organization id for an organization object.') .on("--help", () => {
.on('--help', () => { writeLn("\n Examples:");
writeLn('\n Examples:'); writeLn("");
writeLn(''); writeLn(" bw delete item 7063feab-4b10-472e-b64c-785e2b870b92");
writeLn(' bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02 eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg=='); writeLn(" bw delete item 89c21cd2-fab0-4f69-8c6e-ab8a0168f69a --permanent");
writeLn(' echo \'eyJuYW1lIjoiTXkgRm9sZGVyMiJ9Cg==\' | ' + writeLn(" bw delete folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02");
'bw edit folder 5cdfbd80-d99f-409b-915b-f4c5d0241b02'); writeLn(
writeLn(' bw edit item-collections 78307355-fd25-416b-88b8-b33fd0e88c82 ' + " bw delete attachment b857igwl1dzrs2 --itemid 310d5ffd-e9a2-4451-af87-ea054dce0f78"
'WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=='); );
writeLn('', true); writeLn("", true);
}) })
.action(async (object, id, encodedJson, cmd) => { .action(async (object, id, cmd) => {
if (!this.validateObject(object, editObjects)) { if (!this.validateObject(object, deleteObjects)) {
return; return;
} }
await this.exitIfLocked(); await this.exitIfLocked();
const command = new EditCommand(this.main.cipherService, this.main.folderService, const command = new DeleteCommand(
this.main.cryptoService, this.main.apiService); this.main.cipherService,
const response = await command.run(object, id, encodedJson, cmd); this.main.folderService,
this.processResponse(response); this.main.userService,
}); this.main.apiService
} );
const response = await command.run(object, id, cmd);
this.processResponse(response);
});
}
private deleteCommand(): program.Command { private restoreCommand(): program.Command {
const deleteObjects = [ const restoreObjects = ["item"];
'item', return new program.Command("restore")
'attachment', .arguments("<object> <id>")
'folder', .description("Restores an object from the trash.", {
'org-collection', object: "Valid objects are: " + restoreObjects.join(", "),
]; id: "Object's globally unique `id`.",
return new program.Command('delete') })
.arguments('<object> <id>') .on("--help", () => {
.description('Delete an object from the vault.', { writeLn("\n Examples:");
object: 'Valid objects are: ' + deleteObjects.join(', '), writeLn("");
id: 'Object\'s globally unique `id`.', writeLn(" bw restore item 7063feab-4b10-472e-b64c-785e2b870b92");
}) writeLn("", true);
.option('--itemid <itemid>', 'Attachment\'s item id.') })
.option('--organizationid <organizationid>', 'Organization id for an organization object.') .action(async (object, id, cmd) => {
.option('-p, --permanent', 'Permanently deletes the item instead of soft-deleting it (item only).') if (!this.validateObject(object, restoreObjects)) {
.on('--help', () => { return;
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(); await this.exitIfLocked();
const command = new DeleteCommand(this.main.cipherService, this.main.folderService, const command = new RestoreCommand(this.main.cipherService);
this.main.userService, this.main.apiService); const response = await command.run(object, id, cmd);
const response = await command.run(object, id, cmd); this.processResponse(response);
this.processResponse(response); });
}); }
}
private restoreCommand(): program.Command { private shareCommand(commandName: string, deprecated: boolean): program.Command {
const restoreObjects = [ return new program.Command(commandName)
'item', .arguments("<id> <organizationId> [encodedJson]")
]; .description((deprecated ? "--DEPRECATED-- " : "") + "Move an item to an organization.", {
return new program.Command('restore') id: "Object's globally unique `id`.",
.arguments('<object> <id>') organizationId: "Organization's globally unique `id`.",
.description('Restores an object from the trash.', { encodedJson: "Encoded json of an array of collection ids. Can also be piped into stdin.",
object: 'Valid objects are: ' + restoreObjects.join(', '), })
id: 'Object\'s globally unique `id`.', .on("--help", () => {
}) writeLn("\n Examples:");
.on('--help', () => { writeLn("");
writeLn('\n Examples:'); writeLn(
writeLn(''); " bw " +
writeLn(' bw restore item 7063feab-4b10-472e-b64c-785e2b870b92'); commandName +
writeLn('', true); " 4af958ce-96a7-45d9-beed-1e70fabaa27a " +
}) "6d82949b-b44d-468a-adae-3f3bacb0ea32 WyI5NzQwNTNkMC0zYjMzLTRiOTgtODg2ZS1mZWNmNWM4ZGJhOTYiXQ=="
.action(async (object, id, cmd) => { );
if (!this.validateObject(object, restoreObjects)) { writeLn(
return; " 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, cmd);
this.processResponse(response);
});
}
await this.exitIfLocked(); private confirmCommand(): program.Command {
const command = new RestoreCommand(this.main.cipherService); const confirmObjects = ["org-member"];
const response = await command.run(object, id, cmd); return new program.Command("confirm")
this.processResponse(response); .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;
}
private shareCommand(commandName: string, deprecated: boolean): program.Command { await this.exitIfLocked();
return new program.Command(commandName) const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService);
.arguments('<id> <organizationId> [encodedJson]') const response = await command.run(object, id, cmd);
.description((deprecated ? '--DEPRECATED-- ' : '') + 'Move an item to an organization.', { this.processResponse(response);
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, cmd);
this.processResponse(response);
});
}
private confirmCommand(): program.Command { private importCommand(): program.Command {
const confirmObjects = [ return new program.Command("import")
'org-member', .arguments("[format] [input]")
]; .description("Import vault data from a file.", {
return new program.Command('confirm') format: "The format of [input]",
.arguments('<object> <id>') input: "Filepath to data to import",
.description('Confirm an object to the organization.', { })
object: 'Valid objects are: ' + confirmObjects.join(', '), .option("--formats", "List formats")
id: 'Object\'s globally unique `id`.', .option("--organizationid <organizationid>", "ID of the organization to import to.")
}) .on("--help", () => {
.option('--organizationid <organizationid>', 'Organization id for an organization object.') writeLn("\n Examples:");
.on('--help', () => { writeLn("");
writeLn('\n Examples:'); writeLn(" bw import --formats");
writeLn(''); writeLn(" bw import bitwardencsv ./from/source.csv");
writeLn(' bw confirm org-member 7063feab-4b10-472e-b64c-785e2b870b92 ' + writeLn(" bw import keepass2xml keepass_backup.xml");
'--organizationid 310d5ffd-e9a2-4451-af87-ea054dce0f78'); writeLn(
writeLn('', true); " bw import --organizationid cf14adc3-aca5-4573-890a-f6fa231436d9 keepass2xml keepass_backup.xml"
}) );
.action(async (object, id, cmd) => { })
if (!this.validateObject(object, confirmObjects)) { .action(async (format, filepath, options) => {
return; await this.exitIfLocked();
} const command = new ImportCommand(this.main.importService, this.main.userService);
const response = await command.run(format, filepath, options);
this.processResponse(response);
});
}
await this.exitIfLocked(); private exportCommand(): program.Command {
const command = new ConfirmCommand(this.main.apiService, this.main.cryptoService); return new program.Command("export")
const response = await command.run(object, id, cmd); .arguments("[password]")
this.processResponse(response); .description("Export vault data to a CSV or JSON file.", {
}); password: "Optional: Your master password.",
} })
.option("--output <output>", "Output directory or filename.")
private importCommand(): program.Command { .option("--format <format>", "Export file format.")
return new program.Command('import') .option("--organizationid <organizationid>", "Organization id for an organization.")
.arguments('[format] [input]') .on("--help", () => {
.description('Import vault data from a file.', { writeLn("\n Notes:");
format: 'The format of [input]', writeLn("");
input: 'Filepath to data to import', writeLn(" Valid formats are `csv`, `json`, `encrypted_json`. Default format is `csv`.");
}) writeLn("");
.option('--formats', 'List formats') writeLn(
.option('--organizationid <organizationid>', 'ID of the organization to import to.') " If --raw option is specified and no output filename or directory is given, the"
.on('--help', () => { );
writeLn('\n Examples:'); writeLn(" result is written to stdout.");
writeLn(''); writeLn("");
writeLn(' bw import --formats'); writeLn(" Examples:");
writeLn(' bw import bitwardencsv ./from/source.csv'); writeLn("");
writeLn(' bw import keepass2xml keepass_backup.xml'); writeLn(" bw export");
writeLn(' bw import --organizationid cf14adc3-aca5-4573-890a-f6fa231436d9 keepass2xml keepass_backup.xml'); writeLn(" bw --raw export");
}) writeLn(" bw export myPassword321");
.action(async (format, filepath, options) => { writeLn(" bw export myPassword321 --format json");
await this.exitIfLocked(); writeLn(" bw export --output ./exp/bw.csv");
const command = new ImportCommand(this.main.importService, this.main.userService); writeLn(" bw export myPassword321 --output bw.json --format json");
const response = await command.run(format, filepath, options); writeLn(
this.processResponse(response); " bw export myPassword321 --organizationid 7063feab-4b10-472e-b64c-785e2b870b92"
}); );
} writeLn("", true);
})
private exportCommand(): program.Command { .action(async (password, options) => {
return new program.Command('export') await this.exitIfLocked();
.arguments('[password]') const command = new ExportCommand(
.description('Export vault data to a CSV or JSON file.', { this.main.exportService,
password: 'Optional: Your master password.', this.main.policyService,
}) this.main.keyConnectorService,
.option('--output <output>', 'Output directory or filename.') this.main.userVerificationService
.option('--format <format>', 'Export file format.') );
.option('--organizationid <organizationid>', 'Organization id for an organization.') const response = await command.run(password, options);
.on('--help', () => { this.processResponse(response);
writeLn('\n Notes:'); });
writeLn(''); }
writeLn(' Valid formats are `csv`, `json`, `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 (password, options) => {
await this.exitIfLocked();
const command = new ExportCommand(this.main.exportService, this.main.policyService,
this.main.keyConnectorService, this.main.userVerificationService);
const response = await command.run(password, options);
this.processResponse(response);
});
}
} }

View File

@@ -11,15 +11,9 @@
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"jslib-common/*": [ "jslib-common/*": ["jslib/common/src/*"],
"jslib/common/src/*" "jslib-node/*": ["jslib/node/src/*"]
],
"jslib-node/*": [
"jslib/node/src/*"
]
} }
}, },
"include": [ "include": ["src"]
"src"
]
} }

View File

@@ -1,39 +1,17 @@
{ {
"extends": "tslint:recommended", "extends": "tslint:recommended",
"rules": { "rules": {
"align": [ "align": [true, "statements", "members"],
true,
"statements",
"members"
],
"ban-types": { "ban-types": {
"options": [ "options": [
[ ["Object", "Avoid using the `Object` type. Did you mean `object`?"],
"Object", ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"],
"Avoid using the `Object` type. Did you mean `object`?" ["Number", "Avoid using the `Number` type. Did you mean `number`?"],
], ["String", "Avoid using the `String` type. Did you mean `string`?"],
[ ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"]
"Boolean",
"Avoid using the `Boolean` type. Did you mean `boolean`?"
],
[
"Number",
"Avoid using the `Number` type. Did you mean `number`?"
],
[
"String",
"Avoid using the `String` type. Did you mean `string`?"
],
[
"Symbol",
"Avoid using the `Symbol` type. Did you mean `symbol`?"
]
] ]
}, },
"member-access": [ "member-access": [true, "no-public"],
true,
"no-public"
],
"member-ordering": [ "member-ordering": [
true, true,
{ {
@@ -56,15 +34,9 @@
] ]
} }
], ],
"no-empty": [ "no-empty": [true, "allow-empty-catch"],
true,
"allow-empty-catch"
],
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"object-literal-shorthand": [ "object-literal-shorthand": [true, "never"],
true,
"never"
],
"prefer-for-of": false, "prefer-for-of": false,
"whitespace": [ "whitespace": [
true, true,
@@ -78,7 +50,7 @@
], ],
"max-classes-per-file": false, "max-classes-per-file": false,
"ordered-imports": true, "ordered-imports": true,
"arrow-parens": [ true ], "arrow-parens": [true],
"trailing-comma": [ "trailing-comma": [
true, true,
{ {

View File

@@ -1,72 +1,70 @@
const path = require('path'); const path = require("path");
const webpack = require('webpack'); const webpack = require("webpack");
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require("copy-webpack-plugin");
const nodeExternals = require('webpack-node-externals'); const nodeExternals = require("webpack-node-externals");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
if (process.env.NODE_ENV == null) { if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = 'development'; process.env.NODE_ENV = "development";
} }
const ENV = process.env.ENV = process.env.NODE_ENV; const ENV = (process.env.ENV = process.env.NODE_ENV);
const moduleRules = [ const moduleRules = [
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: 'pre', enforce: "pre",
loader: 'tslint-loader', loader: "tslint-loader",
}, },
{ {
test: /\.ts$/, test: /\.ts$/,
loaders: ['ts-loader'], loaders: ["ts-loader"],
exclude: path.resolve(__dirname, 'node_modules'), exclude: path.resolve(__dirname, "node_modules"),
}, },
]; ];
const plugins = [ const plugins = [
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: [{ from: "./src/locales", to: "locales" }],
{ from: './src/locales', to: 'locales' }, }),
] new webpack.DefinePlugin({
}), "process.env.BWCLI_ENV": JSON.stringify(ENV),
new webpack.DefinePlugin({ }),
'process.env.BWCLI_ENV': JSON.stringify(ENV), new webpack.BannerPlugin({
}), banner: "#!/usr/bin/env node",
new webpack.BannerPlugin({ raw: true,
banner: '#!/usr/bin/env node', }),
raw: true new webpack.IgnorePlugin(/^encoding$/, /node-fetch/),
}),
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/),
]; ];
const config = { const config = {
mode: ENV, mode: ENV,
target: 'node', target: "node",
devtool: ENV === 'development' ? 'eval-source-map' : 'source-map', devtool: ENV === "development" ? "eval-source-map" : "source-map",
node: { node: {
__dirname: false, __dirname: false,
__filename: false, __filename: false,
}, },
entry: { entry: {
'bw': './src/bw.ts', bw: "./src/bw.ts",
}, },
optimization: { optimization: {
minimize: false, minimize: false,
}, },
resolve: { resolve: {
extensions: ['.ts', '.js'], extensions: [".ts", ".js"],
symlinks: false, symlinks: false,
modules: [path.resolve('node_modules')], modules: [path.resolve("node_modules")],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
}, },
output: { output: {
filename: '[name].js', filename: "[name].js",
path: path.resolve(__dirname, 'build'), path: path.resolve(__dirname, "build"),
}, },
module: { rules: moduleRules }, module: { rules: moduleRules },
plugins: plugins, plugins: plugins,
externals: [nodeExternals()], externals: [nodeExternals()],
}; };
module.exports = config; module.exports = config;