mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
* Extract files only used in cli out of libs/node Move commands from libs/node to cli Move program from libs/node to cli Move services from libs/node to cli Move specs from libs/node to cli Naming changes based on ADR 12 Rename commands Rename models/request Rename models/response Remove entries from whitelist-capital-letters.txt * Merge lowDbStorageService into base class Move logic from extended lowdbStorage.service.ts into base-lowdb-storage.service.ts Delete lowdb-storage.service.ts Rename base-lowdb-storage.service.ts to lowdb-storage.service.ts * Merge login.command with base class program.ts - changed import temporarily to make it easier to review Remove passing in clientId, set "cli" when constructing ssoRedirectUri call Remove setting callbacks, use private methods instead Remove i18nService from constructor params Add syncService, keyConnectorService and logoutCallback to constructor Merge successCallback with handleSuccessResponse Remove validatedParams callback and added private method Move options(program.OptionValues) and set in run() Delete login.command.ts * Rename base-login.command.ts to login.command.ts * Merge base.program.ts with program.ts
541 lines
18 KiB
TypeScript
541 lines
18 KiB
TypeScript
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
|
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
|
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
|
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
|
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
|
import { SendType } from "@bitwarden/common/enums/sendType";
|
|
import { Utils } from "@bitwarden/common/misc/utils";
|
|
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
|
import { CardExport } from "@bitwarden/common/models/export/card.export";
|
|
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
|
|
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
|
|
import { FieldExport } from "@bitwarden/common/models/export/field.export";
|
|
import { FolderExport } from "@bitwarden/common/models/export/folder.export";
|
|
import { IdentityExport } from "@bitwarden/common/models/export/identity.export";
|
|
import { LoginUriExport } from "@bitwarden/common/models/export/login-uri.export";
|
|
import { LoginExport } from "@bitwarden/common/models/export/login.export";
|
|
import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export";
|
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
|
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
|
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
|
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
|
|
|
import { OrganizationCollectionRequest } from "../models/request/organization-collection.request";
|
|
import { Response } from "../models/response";
|
|
import { CipherResponse } from "../models/response/cipher.response";
|
|
import { CollectionResponse } from "../models/response/collection.response";
|
|
import { FolderResponse } from "../models/response/folder.response";
|
|
import { OrganizationCollectionResponse } from "../models/response/organization-collection.response";
|
|
import { OrganizationResponse } from "../models/response/organization.response";
|
|
import { SendResponse } from "../models/response/send.response";
|
|
import { StringResponse } from "../models/response/string.response";
|
|
import { TemplateResponse } from "../models/response/template.response";
|
|
import { SelectionReadOnly } from "../models/selection-read-only";
|
|
import { CliUtils } from "../utils";
|
|
|
|
import { DownloadCommand } from "./download.command";
|
|
|
|
export class GetCommand extends DownloadCommand {
|
|
constructor(
|
|
private cipherService: CipherService,
|
|
private folderService: FolderService,
|
|
private collectionService: CollectionService,
|
|
private totpService: TotpService,
|
|
private auditService: AuditService,
|
|
cryptoService: CryptoService,
|
|
private stateService: StateService,
|
|
private searchService: SearchService,
|
|
private apiService: ApiService,
|
|
private organizationService: OrganizationService
|
|
) {
|
|
super(cryptoService);
|
|
}
|
|
|
|
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
|
|
if (id != null) {
|
|
id = id.toLowerCase();
|
|
}
|
|
|
|
const normalizedOptions = new Options(cmdOptions);
|
|
switch (object.toLowerCase()) {
|
|
case "item":
|
|
return await this.getCipher(id);
|
|
case "username":
|
|
return await this.getUsername(id);
|
|
case "password":
|
|
return await this.getPassword(id);
|
|
case "uri":
|
|
return await this.getUri(id);
|
|
case "totp":
|
|
return await this.getTotp(id);
|
|
case "notes":
|
|
return await this.getNotes(id);
|
|
case "exposed":
|
|
return await this.getExposed(id);
|
|
case "attachment":
|
|
return await this.getAttachment(id, normalizedOptions);
|
|
case "folder":
|
|
return await this.getFolder(id);
|
|
case "collection":
|
|
return await this.getCollection(id);
|
|
case "org-collection":
|
|
return await this.getOrganizationCollection(id, normalizedOptions);
|
|
case "organization":
|
|
return await this.getOrganization(id);
|
|
case "template":
|
|
return await this.getTemplate(id);
|
|
case "fingerprint":
|
|
return await this.getFingerprint(id);
|
|
default:
|
|
return Response.badRequest("Unknown object.");
|
|
}
|
|
}
|
|
|
|
private async getCipherView(id: string): Promise<CipherView | CipherView[]> {
|
|
let decCipher: CipherView = null;
|
|
if (Utils.isGuid(id)) {
|
|
const cipher = await this.cipherService.get(id);
|
|
if (cipher != null) {
|
|
decCipher = await cipher.decrypt();
|
|
}
|
|
} else if (id.trim() !== "") {
|
|
let ciphers = await this.cipherService.getAllDecrypted();
|
|
ciphers = this.searchService.searchCiphersBasic(ciphers, id);
|
|
if (ciphers.length > 1) {
|
|
return ciphers;
|
|
}
|
|
if (ciphers.length > 0) {
|
|
decCipher = ciphers[0];
|
|
}
|
|
}
|
|
|
|
return decCipher;
|
|
}
|
|
|
|
private async getCipher(id: string, filter?: (c: CipherView) => boolean) {
|
|
let decCipher = await this.getCipherView(id);
|
|
if (decCipher == null) {
|
|
return Response.notFound();
|
|
}
|
|
if (Array.isArray(decCipher)) {
|
|
if (filter != null) {
|
|
decCipher = decCipher.filter(filter);
|
|
if (decCipher.length === 1) {
|
|
decCipher = decCipher[0];
|
|
}
|
|
}
|
|
if (Array.isArray(decCipher)) {
|
|
return Response.multipleResults(decCipher.map((c) => c.id));
|
|
}
|
|
}
|
|
const res = new CipherResponse(decCipher);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getUsername(id: string) {
|
|
const cipherResponse = await this.getCipher(
|
|
id,
|
|
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.username)
|
|
);
|
|
if (!cipherResponse.success) {
|
|
return cipherResponse;
|
|
}
|
|
|
|
const cipher = cipherResponse.data as CipherResponse;
|
|
if (cipher.type !== CipherType.Login) {
|
|
return Response.badRequest("Not a login.");
|
|
}
|
|
|
|
if (Utils.isNullOrWhitespace(cipher.login.username)) {
|
|
return Response.error("No username available for this login.");
|
|
}
|
|
|
|
const res = new StringResponse(cipher.login.username);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getPassword(id: string) {
|
|
const cipherResponse = await this.getCipher(
|
|
id,
|
|
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.password)
|
|
);
|
|
if (!cipherResponse.success) {
|
|
return cipherResponse;
|
|
}
|
|
|
|
const cipher = cipherResponse.data as CipherResponse;
|
|
if (cipher.type !== CipherType.Login) {
|
|
return Response.badRequest("Not a login.");
|
|
}
|
|
|
|
if (Utils.isNullOrWhitespace(cipher.login.password)) {
|
|
return Response.error("No password available for this login.");
|
|
}
|
|
|
|
const res = new StringResponse(cipher.login.password);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getUri(id: string) {
|
|
const cipherResponse = await this.getCipher(
|
|
id,
|
|
(c) =>
|
|
c.type === CipherType.Login &&
|
|
c.login.uris != null &&
|
|
c.login.uris.length > 0 &&
|
|
c.login.uris[0].uri !== ""
|
|
);
|
|
if (!cipherResponse.success) {
|
|
return cipherResponse;
|
|
}
|
|
|
|
const cipher = cipherResponse.data as CipherResponse;
|
|
if (cipher.type !== CipherType.Login) {
|
|
return Response.badRequest("Not a login.");
|
|
}
|
|
|
|
if (
|
|
cipher.login.uris == null ||
|
|
cipher.login.uris.length === 0 ||
|
|
cipher.login.uris[0].uri === ""
|
|
) {
|
|
return Response.error("No uri available for this login.");
|
|
}
|
|
|
|
const res = new StringResponse(cipher.login.uris[0].uri);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getTotp(id: string) {
|
|
const cipherResponse = await this.getCipher(
|
|
id,
|
|
(c) => c.type === CipherType.Login && !Utils.isNullOrWhitespace(c.login.totp)
|
|
);
|
|
if (!cipherResponse.success) {
|
|
return cipherResponse;
|
|
}
|
|
|
|
const cipher = cipherResponse.data as CipherResponse;
|
|
if (cipher.type !== CipherType.Login) {
|
|
return Response.badRequest("Not a login.");
|
|
}
|
|
|
|
if (Utils.isNullOrWhitespace(cipher.login.totp)) {
|
|
return Response.error("No TOTP available for this login.");
|
|
}
|
|
|
|
const totp = await this.totpService.getCode(cipher.login.totp);
|
|
if (totp == null) {
|
|
return Response.error("Couldn't generate TOTP code.");
|
|
}
|
|
|
|
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
|
if (!canAccessPremium) {
|
|
const originalCipher = await this.cipherService.get(cipher.id);
|
|
if (
|
|
originalCipher == null ||
|
|
originalCipher.organizationId == null ||
|
|
!originalCipher.organizationUseTotp
|
|
) {
|
|
return Response.error("Premium status is required to use this feature.");
|
|
}
|
|
}
|
|
|
|
const res = new StringResponse(totp);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getNotes(id: string) {
|
|
const cipherResponse = await this.getCipher(id, (c) => !Utils.isNullOrWhitespace(c.notes));
|
|
if (!cipherResponse.success) {
|
|
return cipherResponse;
|
|
}
|
|
|
|
const cipher = cipherResponse.data as CipherResponse;
|
|
if (Utils.isNullOrWhitespace(cipher.notes)) {
|
|
return Response.error("No notes available for this item.");
|
|
}
|
|
|
|
const res = new StringResponse(cipher.notes);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getExposed(id: string) {
|
|
const passwordResponse = await this.getPassword(id);
|
|
if (!passwordResponse.success) {
|
|
return passwordResponse;
|
|
}
|
|
|
|
const exposedNumber = await this.auditService.passwordLeaked(
|
|
(passwordResponse.data as StringResponse).data
|
|
);
|
|
const res = new StringResponse(exposedNumber.toString());
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getAttachment(id: string, options: Options) {
|
|
if (options.itemId == null || options.itemId === "") {
|
|
return Response.badRequest("--itemid <itemid> required.");
|
|
}
|
|
|
|
const itemId = options.itemId.toLowerCase();
|
|
const cipherResponse = await this.getCipher(itemId);
|
|
if (!cipherResponse.success) {
|
|
return cipherResponse;
|
|
}
|
|
|
|
const cipher = await this.getCipherView(itemId);
|
|
if (
|
|
cipher == null ||
|
|
Array.isArray(cipher) ||
|
|
cipher.attachments == null ||
|
|
cipher.attachments.length === 0
|
|
) {
|
|
return Response.error("No attachments available for this item.");
|
|
}
|
|
|
|
let attachments = cipher.attachments.filter(
|
|
(a) =>
|
|
a.id.toLowerCase() === id ||
|
|
(a.fileName != null && a.fileName.toLowerCase().indexOf(id) > -1)
|
|
);
|
|
if (attachments.length === 0) {
|
|
return Response.error("Attachment `" + id + "` was not found.");
|
|
}
|
|
|
|
const exactMatches = attachments.filter((a) => a.fileName.toLowerCase() === id);
|
|
if (exactMatches.length === 1) {
|
|
attachments = exactMatches;
|
|
}
|
|
|
|
if (attachments.length > 1) {
|
|
return Response.multipleResults(attachments.map((a) => a.id));
|
|
}
|
|
|
|
if (!(await this.stateService.getCanAccessPremium())) {
|
|
const originalCipher = await this.cipherService.get(cipher.id);
|
|
if (originalCipher == null || originalCipher.organizationId == null) {
|
|
return Response.error("Premium status is required to use this feature.");
|
|
}
|
|
}
|
|
|
|
let url: string;
|
|
try {
|
|
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
|
cipher.id,
|
|
attachments[0].id
|
|
);
|
|
url = attachmentDownloadResponse.url;
|
|
} catch (e) {
|
|
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
|
url = attachments[0].url;
|
|
} else if (e instanceof ErrorResponse) {
|
|
throw new Error((e as ErrorResponse).getSingleMessage());
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const key =
|
|
attachments[0].key != null
|
|
? attachments[0].key
|
|
: await this.cryptoService.getOrgKey(cipher.organizationId);
|
|
return await this.saveAttachmentToFile(url, key, attachments[0].fileName, options.output);
|
|
}
|
|
|
|
private async getFolder(id: string) {
|
|
let decFolder: FolderView = null;
|
|
if (Utils.isGuid(id)) {
|
|
const folder = await this.folderService.getFromState(id);
|
|
if (folder != null) {
|
|
decFolder = await folder.decrypt();
|
|
}
|
|
} else if (id.trim() !== "") {
|
|
let folders = await this.folderService.getAllDecryptedFromState();
|
|
folders = CliUtils.searchFolders(folders, id);
|
|
if (folders.length > 1) {
|
|
return Response.multipleResults(folders.map((f) => f.id));
|
|
}
|
|
if (folders.length > 0) {
|
|
decFolder = folders[0];
|
|
}
|
|
}
|
|
|
|
if (decFolder == null) {
|
|
return Response.notFound();
|
|
}
|
|
const res = new FolderResponse(decFolder);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getCollection(id: string) {
|
|
let decCollection: CollectionView = null;
|
|
if (Utils.isGuid(id)) {
|
|
const collection = await this.collectionService.get(id);
|
|
if (collection != null) {
|
|
decCollection = await collection.decrypt();
|
|
}
|
|
} else if (id.trim() !== "") {
|
|
let collections = await this.collectionService.getAllDecrypted();
|
|
collections = CliUtils.searchCollections(collections, id);
|
|
if (collections.length > 1) {
|
|
return Response.multipleResults(collections.map((c) => c.id));
|
|
}
|
|
if (collections.length > 0) {
|
|
decCollection = collections[0];
|
|
}
|
|
}
|
|
|
|
if (decCollection == null) {
|
|
return Response.notFound();
|
|
}
|
|
const res = new CollectionResponse(decCollection);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getOrganizationCollection(id: string, options: Options) {
|
|
if (options.organizationId == null || options.organizationId === "") {
|
|
return Response.badRequest("`organizationid` option is required.");
|
|
}
|
|
if (!Utils.isGuid(id)) {
|
|
return Response.badRequest("`" + id + "` is not a GUID.");
|
|
}
|
|
if (!Utils.isGuid(options.organizationId)) {
|
|
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
|
|
}
|
|
try {
|
|
const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
|
|
if (orgKey == null) {
|
|
throw new Error("No encryption key for this organization.");
|
|
}
|
|
|
|
const response = await this.apiService.getCollectionDetails(options.organizationId, id);
|
|
const decCollection = new CollectionView(response);
|
|
decCollection.name = await this.cryptoService.decryptToUtf8(
|
|
new EncString(response.name),
|
|
orgKey
|
|
);
|
|
const groups =
|
|
response.groups == null
|
|
? null
|
|
: response.groups.map((g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords));
|
|
const res = new OrganizationCollectionResponse(decCollection, groups);
|
|
return Response.success(res);
|
|
} catch (e) {
|
|
return Response.error(e);
|
|
}
|
|
}
|
|
|
|
private async getOrganization(id: string) {
|
|
let org: Organization = null;
|
|
if (Utils.isGuid(id)) {
|
|
org = await this.organizationService.getFromState(id);
|
|
} else if (id.trim() !== "") {
|
|
let orgs = await this.organizationService.getAll();
|
|
orgs = CliUtils.searchOrganizations(orgs, id);
|
|
if (orgs.length > 1) {
|
|
return Response.multipleResults(orgs.map((c) => c.id));
|
|
}
|
|
if (orgs.length > 0) {
|
|
org = orgs[0];
|
|
}
|
|
}
|
|
|
|
if (org == null) {
|
|
return Response.notFound();
|
|
}
|
|
const res = new OrganizationResponse(org);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getTemplate(id: string) {
|
|
let template: any = null;
|
|
switch (id.toLowerCase()) {
|
|
case "item":
|
|
template = CipherExport.template();
|
|
break;
|
|
case "item.field":
|
|
template = FieldExport.template();
|
|
break;
|
|
case "item.login":
|
|
template = LoginExport.template();
|
|
break;
|
|
case "item.login.uri":
|
|
template = LoginUriExport.template();
|
|
break;
|
|
case "item.card":
|
|
template = CardExport.template();
|
|
break;
|
|
case "item.identity":
|
|
template = IdentityExport.template();
|
|
break;
|
|
case "item.securenote":
|
|
template = SecureNoteExport.template();
|
|
break;
|
|
case "folder":
|
|
template = FolderExport.template();
|
|
break;
|
|
case "collection":
|
|
template = CollectionExport.template();
|
|
break;
|
|
case "item-collections":
|
|
template = ["collection-id1", "collection-id2"];
|
|
break;
|
|
case "org-collection":
|
|
template = OrganizationCollectionRequest.template();
|
|
break;
|
|
case "send.text":
|
|
template = SendResponse.template(SendType.Text);
|
|
break;
|
|
case "send.file":
|
|
template = SendResponse.template(SendType.File);
|
|
break;
|
|
default:
|
|
return Response.badRequest("Unknown template object.");
|
|
}
|
|
|
|
const res = new TemplateResponse(template);
|
|
return Response.success(res);
|
|
}
|
|
|
|
private async getFingerprint(id: string) {
|
|
let fingerprint: string[] = null;
|
|
if (id === "me") {
|
|
fingerprint = await this.cryptoService.getFingerprint(await this.stateService.getUserId());
|
|
} else if (Utils.isGuid(id)) {
|
|
try {
|
|
const response = await this.apiService.getUserPublicKey(id);
|
|
const pubKey = Utils.fromB64ToArray(response.publicKey);
|
|
fingerprint = await this.cryptoService.getFingerprint(id, pubKey.buffer);
|
|
} catch {
|
|
// eslint-disable-next-line
|
|
}
|
|
}
|
|
|
|
if (fingerprint == null) {
|
|
return Response.notFound();
|
|
}
|
|
const res = new StringResponse(fingerprint.join("-"));
|
|
return Response.success(res);
|
|
}
|
|
}
|
|
|
|
class Options {
|
|
itemId: string;
|
|
organizationId: string;
|
|
output: string;
|
|
|
|
constructor(passedOptions: Record<string, any>) {
|
|
this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
|
|
this.itemId = passedOptions?.itemid || passedOptions?.itemId;
|
|
this.output = passedOptions?.output;
|
|
}
|
|
}
|