diff --git a/src/bw.ts b/src/bw.ts index c14b5bc52d..0ad48032a4 100644 --- a/src/bw.ts +++ b/src/bw.ts @@ -17,6 +17,7 @@ import { CryptoService } from 'jslib/services/crypto.service'; import { EnvironmentService } from 'jslib/services/environment.service'; import { ExportService } from 'jslib/services/export.service'; import { FolderService } from 'jslib/services/folder.service'; +import { ImportService } from 'jslib/services/import.service'; import { LockService } from 'jslib/services/lock.service'; import { LowdbStorageService } from 'jslib/services/lowdbStorage.service'; import { NodeApiService } from 'jslib/services/nodeApi.service'; @@ -53,6 +54,7 @@ export class Main { totpService: TotpService; containerService: ContainerService; auditService: AuditService; + importService: ImportService; exportService: ExportService; cryptoFunctionService: NodeCryptoFunctionService; authService: AuthService; @@ -99,6 +101,8 @@ export class Main { this.storageService, this.messagingService, async (expired: boolean) => await this.logout()); this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService); this.totpService = new TotpService(this.storageService, this.cryptoFunctionService); + this.importService = new ImportService(this.cipherService, this.folderService, this.apiService, + this.i18nService); this.exportService = new ExportService(this.folderService, this.cipherService, this.apiService); this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService, this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, true); diff --git a/src/commands/create.command.ts b/src/commands/create.command.ts index e8f01aecd3..69fd8e5411 100644 --- a/src/commands/create.command.ts +++ b/src/commands/create.command.ts @@ -32,7 +32,7 @@ export class CreateCommand { } try { - const reqJson = new Buffer(requestJson, 'base64').toString(); + const reqJson = Buffer.from(requestJson, 'base64').toString(); req = JSON.parse(reqJson); } catch (e) { return Response.badRequest('Error parsing the encoded request data.'); diff --git a/src/commands/edit.command.ts b/src/commands/edit.command.ts index 58ee9e311f..1947758fc1 100644 --- a/src/commands/edit.command.ts +++ b/src/commands/edit.command.ts @@ -26,7 +26,7 @@ export class EditCommand { let req: any = null; try { - const reqJson = new Buffer(requestJson, 'base64').toString(); + const reqJson = Buffer.from(requestJson, 'base64').toString(); req = JSON.parse(reqJson); } catch (e) { return Response.badRequest('Error parsing the encoded request data.'); diff --git a/src/commands/encode.command.ts b/src/commands/encode.command.ts index 1453036ad4..3e310ca031 100644 --- a/src/commands/encode.command.ts +++ b/src/commands/encode.command.ts @@ -11,7 +11,7 @@ export class EncodeCommand { return Response.badRequest('No stdin was piped in.'); } const input = await CliUtils.readStdin(); - const b64 = new Buffer(input, 'utf8').toString('base64'); + const b64 = Buffer.from(input, 'utf8').toString('base64'); const res = new StringResponse(b64); return Response.success(res); } diff --git a/src/commands/get.command.ts b/src/commands/get.command.ts index 39e363a8ae..04680557cd 100644 --- a/src/commands/get.command.ts +++ b/src/commands/get.command.ts @@ -244,7 +244,7 @@ export class GetCommand { const buf = await response.arrayBuffer(); const key = await this.cryptoService.getOrgKey(cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - const filePath = await CliUtils.saveFile(new Buffer(decBuf), cmd.output, attachments[0].fileName); + const filePath = await CliUtils.saveFile(Buffer.from(decBuf), cmd.output, attachments[0].fileName); const res = new MessageResponse('Saved ' + filePath, null); res.raw = filePath; return Response.success(res); diff --git a/src/commands/import.command.ts b/src/commands/import.command.ts new file mode 100644 index 0000000000..3612bca8b9 --- /dev/null +++ b/src/commands/import.command.ts @@ -0,0 +1,67 @@ +import * as program from 'commander'; +import * as inquirer from 'inquirer'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { ImportOptions, ImportService } from 'jslib/abstractions/import.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { Response } from '../models/response'; + +import { CliUtils } from '../utils'; + +const writeLn = CliUtils.writeLn; + +export class ImportCommand { + constructor(private cryptoService: CryptoService, private userService: UserService, + private importService: ImportService) { } + + async list() { + const options: ImportOptions = this.importService.getOptions().sort((a, b) => { + if (a.id < b.id) { return -1; } + if (a.id > b.id) { return 1; } + return 0; + }); + writeLn('\nSupported input formats:\n'); + options.forEach((option) => { + writeLn(' ' + option.id); + }); + return Response.success(); + } + + async run(type: string, path: string, password: string, cmd: program.Command): Promise { + if (password == null || password === '') { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'password', + name: 'password', + message: 'Master password:', + mask: '*', + }); + password = answer.password; + } + if (password == null || password === '') { + return Response.badRequest('Master password is required.'); + } + + const email = await this.userService.getEmail(); + const key = await this.cryptoService.makeKey(password, email); + const keyHash = await this.cryptoService.hashPassword(password, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + const importer = await this.importService.getImporter(type); + if (importer === null) { + return Response.badRequest('Proper importer type required.'); + } + if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + return CliUtils.readFile(path).then(async (contents) => { + const submitResult = await this.importService.submit(importer, contents); + if (submitResult !== null) { + return Response.success(); + } else { + return Response.badRequest(submitResult.message); + } + }).catch((err) => { + return Response.badRequest(err); + }); + } else { + return Response.badRequest('Invalid master password.'); + } + } +} diff --git a/src/program.ts b/src/program.ts index 83664ea282..a7cd3006bd 100644 --- a/src/program.ts +++ b/src/program.ts @@ -11,6 +11,7 @@ import { EncodeCommand } from './commands/encode.command'; import { ExportCommand } from './commands/export.command'; import { GenerateCommand } from './commands/generate.command'; import { GetCommand } from './commands/get.command'; +import { ImportCommand } from './commands/import.command'; import { ListCommand } from './commands/list.command'; import { LockCommand } from './commands/lock.command'; import { LoginCommand } from './commands/login.command'; @@ -24,16 +25,10 @@ import { ListResponse } from './models/response/listResponse'; import { MessageResponse } from './models/response/messageResponse'; import { StringResponse } from './models/response/stringResponse'; import { TemplateResponse } from './models/response/templateResponse'; +import { CliUtils } from './utils'; const chalk = chk.default; - -function writeLn(s: string, finalLine: boolean = false) { - if (finalLine && process.platform === 'win32') { - process.stdout.write(s); - } else { - process.stdout.write(s + '\n'); - } -} +const writeLn = CliUtils.writeLn; export class Program { constructor(private main: Main) { } @@ -395,6 +390,31 @@ export class Program { this.processResponse(response); }); + program + .command('import [password]') + .description('Import vault data from a file.') + .option('-l, --list-formats', 'List valid formats') + .on('option:list-formats', async () => { + const command = new ImportCommand(this.main.cryptoService, + this.main.userService, this.main.importService); + const response = await command.list(); + this.processResponse(response); + }) + .on('--help', () => { + writeLn('\n Examples:'); + writeLn(''); + writeLn(' bw import --list-formats'); + writeLn(' bw import bitwardencsv ./from/source.csv'); + writeLn(' bw import keepass2xml keepass_backup.xml myPassword123'); + }) + .action(async (format, filepath, password, cmd) => { + await this.exitIfLocked(); + const command = new ImportCommand(this.main.cryptoService, + this.main.userService, this.main.importService); + const response = await command.run(format, filepath, password, cmd); + this.processResponse(response); + }); + program .command('generate') .description('Generate a password.') diff --git a/src/utils.ts b/src/utils.ts index c1a540b27c..6063deecde 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,6 +9,36 @@ import { FolderView } from 'jslib/models/view/folderView'; import { NodeUtils } from 'jslib/misc/nodeUtils'; export class CliUtils { + static writeLn(s: string, finalLine: boolean = false) { + if (finalLine && process.platform === 'win32') { + process.stdout.write(s); + } else { + process.stdout.write(s + '\n'); + } + } + + static readFile(input: string): Promise { + return new Promise((resolve, reject) => { + let p: string = null; + if (input !== null && input !== '') { + const osInput = path.join(input); + if (osInput.indexOf(path.sep) !== 0) { + p = path.join(process.cwd(), osInput); + } else { + p = osInput; + } + } else { + reject('you must specify a path'); + } + fs.readFile(p, 'utf8', (err, data) => { + if (err != null) { + reject(err.message); + } + resolve(data); + }); + }); + } + static saveFile(data: string | Buffer, output: string, defaultFileName: string) { let p: string = null; let mkdir = false;