mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-06 09:20:43 +01:00
Add send to cli (#253)
* Upgrade commander to 7.0.0 * Add url to Api call This is needed to allow access to sends that are available from a different Bitwarden server than configured for the CLI * Allow upload of send files from CLI * Allow send search by accessId * Utils methods used in Send CLI implementation * Revert adding string type to encrypted file data * linter fixes * Add Buffer to ArrayBuffer used in CLI send implementation
This commit is contained in:
parent
06239aea2d
commit
09c444ddd4
526
package-lock.json
generated
526
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -79,7 +79,7 @@
|
|||||||
"big-integer": "1.6.36",
|
"big-integer": "1.6.36",
|
||||||
"browser-hrtime": "^1.1.8",
|
"browser-hrtime": "^1.1.8",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.1",
|
||||||
"commander": "2.18.0",
|
"commander": "7.0.0",
|
||||||
"core-js": "2.6.2",
|
"core-js": "2.6.2",
|
||||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||||
"electron-log": "4.3.0",
|
"electron-log": "4.3.0",
|
||||||
|
@ -174,7 +174,7 @@ export abstract class ApiService {
|
|||||||
deleteFolder: (id: string) => Promise<any>;
|
deleteFolder: (id: string) => Promise<any>;
|
||||||
|
|
||||||
getSend: (id: string) => Promise<SendResponse>;
|
getSend: (id: string) => Promise<SendResponse>;
|
||||||
postSendAccess: (id: string, request: SendAccessRequest) => Promise<SendAccessResponse>;
|
postSendAccess: (id: string, request: SendAccessRequest, apiUrl?: string) => Promise<SendAccessResponse>;
|
||||||
getSends: () => Promise<ListResponse<SendResponse>>;
|
getSends: () => Promise<ListResponse<SendResponse>>;
|
||||||
postSend: (request: SendRequest) => Promise<SendResponse>;
|
postSend: (request: SendRequest) => Promise<SendResponse>;
|
||||||
postSendFile: (data: FormData) => Promise<SendResponse>;
|
postSendFile: (data: FormData) => Promise<SendResponse>;
|
||||||
|
@ -9,7 +9,7 @@ export abstract class SendService {
|
|||||||
decryptedSendCache: SendView[];
|
decryptedSendCache: SendView[];
|
||||||
|
|
||||||
clearCache: () => void;
|
clearCache: () => void;
|
||||||
encrypt: (model: SendView, file: File, password: string, key?: SymmetricCryptoKey) => Promise<[Send, ArrayBuffer]>;
|
encrypt: (model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey) => Promise<[Send, ArrayBuffer]>;
|
||||||
get: (id: string) => Promise<Send>;
|
get: (id: string) => Promise<Send>;
|
||||||
getAll: () => Promise<Send[]>;
|
getAll: () => Promise<Send[]>;
|
||||||
getAllDecrypted: () => Promise<SendView[]>;
|
getAllDecrypted: () => Promise<SendView[]>;
|
||||||
|
@ -41,7 +41,7 @@ export class LoginCommand {
|
|||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(email: string, password: string, cmd: program.Command) {
|
async run(email: string, password: string, options: program.OptionValues) {
|
||||||
this.canInteract = process.env.BW_NOINTERACTION !== 'true';
|
this.canInteract = process.env.BW_NOINTERACTION !== 'true';
|
||||||
|
|
||||||
let ssoCodeVerifier: string = null;
|
let ssoCodeVerifier: string = null;
|
||||||
@ -50,7 +50,7 @@ export class LoginCommand {
|
|||||||
let clientId: string = null;
|
let clientId: string = null;
|
||||||
let clientSecret: string = null;
|
let clientSecret: string = null;
|
||||||
|
|
||||||
if (cmd.apikey != null) {
|
if (options.apikey != null) {
|
||||||
const storedClientId: string = process.env.BW_CLIENTID;
|
const storedClientId: string = process.env.BW_CLIENTID;
|
||||||
const storedClientSecret: string = process.env.BW_CLIENTSECRET;
|
const storedClientSecret: string = process.env.BW_CLIENTSECRET;
|
||||||
if (storedClientId == null) {
|
if (storedClientId == null) {
|
||||||
@ -77,7 +77,7 @@ export class LoginCommand {
|
|||||||
} else {
|
} else {
|
||||||
clientSecret = storedClientSecret;
|
clientSecret = storedClientSecret;
|
||||||
}
|
}
|
||||||
} else if (cmd.sso != null && this.canInteract) {
|
} else if (options.sso != null && this.canInteract) {
|
||||||
const passwordOptions: any = {
|
const passwordOptions: any = {
|
||||||
type: 'password',
|
type: 'password',
|
||||||
length: 64,
|
length: 64,
|
||||||
@ -112,10 +112,10 @@ export class LoginCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (password == null || password === '') {
|
if (password == null || password === '') {
|
||||||
if (cmd.passwordfile) {
|
if (options.passwordfile) {
|
||||||
password = await NodeUtils.readFirstLine(cmd.passwordfile);
|
password = await NodeUtils.readFirstLine(options.passwordfile);
|
||||||
} else if (cmd.passwordenv && process.env[cmd.passwordenv]) {
|
} else if (options.passwordenv && process.env[options.passwordenv]) {
|
||||||
password = process.env[cmd.passwordenv];
|
password = process.env[options.passwordenv];
|
||||||
} else if (this.canInteract) {
|
} else if (this.canInteract) {
|
||||||
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||||
type: 'password',
|
type: 'password',
|
||||||
@ -131,11 +131,11 @@ export class LoginCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let twoFactorToken: string = cmd.code;
|
let twoFactorToken: string = options.code;
|
||||||
let twoFactorMethod: TwoFactorProviderType = null;
|
let twoFactorMethod: TwoFactorProviderType = null;
|
||||||
try {
|
try {
|
||||||
if (cmd.method != null) {
|
if (options.method != null) {
|
||||||
twoFactorMethod = parseInt(cmd.method, null);
|
twoFactorMethod = parseInt(options.method, null);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.error('Invalid two-step login method.');
|
return Response.error('Invalid two-step login method.');
|
||||||
@ -185,18 +185,18 @@ export class LoginCommand {
|
|||||||
if (twoFactorProviders.length === 1) {
|
if (twoFactorProviders.length === 1) {
|
||||||
selectedProvider = twoFactorProviders[0];
|
selectedProvider = twoFactorProviders[0];
|
||||||
} else if (this.canInteract) {
|
} else if (this.canInteract) {
|
||||||
const options = twoFactorProviders.map((p) => p.name);
|
const twoFactorOptions = twoFactorProviders.map((p) => p.name);
|
||||||
options.push(new inquirer.Separator());
|
twoFactorOptions.push(new inquirer.Separator());
|
||||||
options.push('Cancel');
|
twoFactorOptions.push('Cancel');
|
||||||
const answer: inquirer.Answers =
|
const answer: inquirer.Answers =
|
||||||
await inquirer.createPromptModule({ output: process.stderr })({
|
await inquirer.createPromptModule({ output: process.stderr })({
|
||||||
type: 'list',
|
type: 'list',
|
||||||
name: 'method',
|
name: 'method',
|
||||||
message: 'Two-step login method:',
|
message: 'Two-step login method:',
|
||||||
choices: options,
|
choices: twoFactorOptions,
|
||||||
});
|
});
|
||||||
const i = options.indexOf(answer.method);
|
const i = twoFactorOptions.indexOf(answer.method);
|
||||||
if (i === (options.length - 1)) {
|
if (i === (twoFactorOptions.length - 1)) {
|
||||||
return Response.error('Login failed.');
|
return Response.error('Login failed.');
|
||||||
}
|
}
|
||||||
selectedProvider = twoFactorProviders[i];
|
selectedProvider = twoFactorProviders[i];
|
||||||
|
@ -15,7 +15,7 @@ export class UpdateCommand {
|
|||||||
this.inPkg = !!(process as any).pkg;
|
this.inPkg = !!(process as any).pkg;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(cmd: program.Command): Promise<Response> {
|
async run(): Promise<Response> {
|
||||||
const currentVersion = this.platformUtilsService.getApplicationVersion();
|
const currentVersion = this.platformUtilsService.getApplicationVersion();
|
||||||
|
|
||||||
const response = await fetch.default('https://api.github.com/repos/bitwarden/' +
|
const response = await fetch.default('https://api.github.com/repos/bitwarden/' +
|
||||||
|
@ -26,4 +26,9 @@ export class NodeUtils {
|
|||||||
.on('error', (err) => reject(err));
|
.on('error', (err) => reject(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/31394257
|
||||||
|
static bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
|
||||||
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,6 +273,14 @@ export class Utils {
|
|||||||
return str == null || typeof str !== 'string' || str.trim() === '';
|
return str == null || typeof str !== 'string' || str.trim() === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static nameOf<T>(name: string & keyof T) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static assign<T>(target: T, source: Partial<T>): T {
|
||||||
|
return Object.assign(target, source);
|
||||||
|
}
|
||||||
|
|
||||||
private static validIpAddress(ipString: string): boolean {
|
private static validIpAddress(ipString: string): boolean {
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
@ -73,6 +73,7 @@ import { VerifyBankRequest } from '../models/request/verifyBankRequest';
|
|||||||
import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest';
|
import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest';
|
||||||
import { VerifyEmailRequest } from '../models/request/verifyEmailRequest';
|
import { VerifyEmailRequest } from '../models/request/verifyEmailRequest';
|
||||||
|
|
||||||
|
import { Utils } from '../misc/utils';
|
||||||
import { ApiKeyResponse } from '../models/response/apiKeyResponse';
|
import { ApiKeyResponse } from '../models/response/apiKeyResponse';
|
||||||
import { BillingResponse } from '../models/response/billingResponse';
|
import { BillingResponse } from '../models/response/billingResponse';
|
||||||
import { BreachAccountResponse } from '../models/response/breachAccountResponse';
|
import { BreachAccountResponse } from '../models/response/breachAccountResponse';
|
||||||
@ -410,8 +411,8 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
return new SendResponse(r);
|
return new SendResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
async postSendAccess(id: string, request: SendAccessRequest): Promise<SendAccessResponse> {
|
async postSendAccess(id: string, request: SendAccessRequest, apiUrl?: string): Promise<SendAccessResponse> {
|
||||||
const r = await this.send('POST', '/sends/access/' + id, request, false, true);
|
const r = await this.send('POST', '/sends/access/' + id, request, false, true, apiUrl);
|
||||||
return new SendAccessResponse(r);
|
return new SendAccessResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1210,7 +1211,8 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any,
|
private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any,
|
||||||
authed: boolean, hasResponse: boolean): Promise<any> {
|
authed: boolean, hasResponse: boolean, apiUrl?: string): Promise<any> {
|
||||||
|
apiUrl = Utils.isNullOrWhitespace(apiUrl) ? this.apiBaseUrl : apiUrl;
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
'Device-Type': this.deviceType,
|
'Device-Type': this.deviceType,
|
||||||
});
|
});
|
||||||
@ -1246,7 +1248,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestInit.headers = headers;
|
requestInit.headers = headers;
|
||||||
const response = await this.fetch(new Request(this.apiBaseUrl + path, requestInit));
|
const response = await this.fetch(new Request(apiUrl + path, requestInit));
|
||||||
|
|
||||||
if (hasResponse && response.status === 200) {
|
if (hasResponse && response.status === 200) {
|
||||||
const responseJson = await response.json();
|
const responseJson = await response.json();
|
||||||
|
@ -169,7 +169,7 @@ export class SearchService implements SearchServiceAbstraction {
|
|||||||
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
|
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (query.length >= 8 && (s.id.startsWith(query) || (s.file?.id != null && s.file.id.startsWith(query)))) {
|
if (query.length >= 8 && (s.id.startsWith(query) || s.accessId.toLocaleLowerCase().startsWith(query) || (s.file?.id != null && s.file.id.startsWith(query)))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
|
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
|
||||||
|
@ -22,6 +22,7 @@ import { StorageService } from '../abstractions/storage.service';
|
|||||||
import { UserService } from '../abstractions/user.service';
|
import { UserService } from '../abstractions/user.service';
|
||||||
|
|
||||||
import { Utils } from '../misc/utils';
|
import { Utils } from '../misc/utils';
|
||||||
|
import { CipherString } from '../models/domain';
|
||||||
|
|
||||||
const Keys = {
|
const Keys = {
|
||||||
sendsPrefix: 'sends_',
|
sendsPrefix: 'sends_',
|
||||||
@ -38,7 +39,7 @@ export class SendService implements SendServiceAbstraction {
|
|||||||
this.decryptedSendCache = null;
|
this.decryptedSendCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async encrypt(model: SendView, file: File, password: string,
|
async encrypt(model: SendView, file: File | ArrayBuffer, password: string,
|
||||||
key?: SymmetricCryptoKey): Promise<[Send, ArrayBuffer]> {
|
key?: SymmetricCryptoKey): Promise<[Send, ArrayBuffer]> {
|
||||||
let fileData: ArrayBuffer = null;
|
let fileData: ArrayBuffer = null;
|
||||||
const send = new Send();
|
const send = new Send();
|
||||||
@ -64,7 +65,13 @@ export class SendService implements SendServiceAbstraction {
|
|||||||
} else if (send.type === SendType.File) {
|
} else if (send.type === SendType.File) {
|
||||||
send.file = new SendFile();
|
send.file = new SendFile();
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
fileData = await this.parseFile(send, file, model.cryptoKey);
|
if (file instanceof ArrayBuffer) {
|
||||||
|
const [name, data] = await this.encryptFileData(model.file.fileName, file, model.cryptoKey);
|
||||||
|
send.file.fileName = name;
|
||||||
|
fileData = data;
|
||||||
|
} else {
|
||||||
|
fileData = await this.parseFile(send, file, model.cryptoKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,9 +234,9 @@ export class SendService implements SendServiceAbstraction {
|
|||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
reader.onload = async (evt) => {
|
reader.onload = async (evt) => {
|
||||||
try {
|
try {
|
||||||
send.file.fileName = await this.cryptoService.encrypt(file.name, key);
|
const [name, data] = await this.encryptFileData(file.name, evt.target.result as ArrayBuffer, key);
|
||||||
const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer, key);
|
send.file.fileName = name;
|
||||||
resolve(fileData);
|
resolve(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
@ -239,4 +246,11 @@ export class SendService implements SendServiceAbstraction {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async encryptFileData(fileName: string, data: ArrayBuffer,
|
||||||
|
key: SymmetricCryptoKey): Promise<[CipherString, ArrayBuffer]> {
|
||||||
|
const encFileName = await this.cryptoService.encrypt(fileName, key);
|
||||||
|
const encFileData = await this.cryptoService.encryptToBytes(data, key);
|
||||||
|
return [encFileName, encFileData];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user