1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

Split jslib into multiple modules (#363)

* Split jslib into multiple modules
This commit is contained in:
Oscar Hinton
2021-06-03 18:58:57 +02:00
committed by GitHub
parent b1d9b84eae
commit 1016bbfb9e
509 changed files with 8838 additions and 1887 deletions

View File

@@ -0,0 +1,543 @@
import {
CdkDragDrop,
moveItemInArray,
} from '@angular/cdk/drag-drop';
import {
Directive,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType';
import { CipherType } from 'jslib-common/enums/cipherType';
import { EventType } from 'jslib-common/enums/eventType';
import { FieldType } from 'jslib-common/enums/fieldType';
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
import { PolicyType } from 'jslib-common/enums/policyType';
import { SecureNoteType } from 'jslib-common/enums/secureNoteType';
import { UriMatchType } from 'jslib-common/enums/uriMatchType';
import { AuditService } from 'jslib-common/abstractions/audit.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { EventService } from 'jslib-common/abstractions/event.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { Cipher } from 'jslib-common/models/domain/cipher';
import { CardView } from 'jslib-common/models/view/cardView';
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { FieldView } from 'jslib-common/models/view/fieldView';
import { FolderView } from 'jslib-common/models/view/folderView';
import { IdentityView } from 'jslib-common/models/view/identityView';
import { LoginUriView } from 'jslib-common/models/view/loginUriView';
import { LoginView } from 'jslib-common/models/view/loginView';
import { SecureNoteView } from 'jslib-common/models/view/secureNoteView';
import { Utils } from 'jslib-common/misc/utils';
@Directive()
export class AddEditComponent implements OnInit {
@Input() cloneMode: boolean = false;
@Input() folderId: string = null;
@Input() cipherId: string;
@Input() type: CipherType;
@Input() collectionIds: string[];
@Input() organizationId: string = null;
@Output() onSavedCipher = new EventEmitter<CipherView>();
@Output() onDeletedCipher = new EventEmitter<CipherView>();
@Output() onRestoredCipher = new EventEmitter<CipherView>();
@Output() onCancelled = new EventEmitter<CipherView>();
@Output() onEditAttachments = new EventEmitter<CipherView>();
@Output() onShareCipher = new EventEmitter<CipherView>();
@Output() onEditCollections = new EventEmitter<CipherView>();
@Output() onGeneratePassword = new EventEmitter();
editMode: boolean = false;
cipher: CipherView;
folders: FolderView[];
collections: CollectionView[] = [];
title: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
restorePromise: Promise<any>;
checkPasswordPromise: Promise<number>;
showPassword: boolean = false;
showCardNumber: boolean = false;
showCardCode: boolean = false;
cipherType = CipherType;
fieldType = FieldType;
addFieldType: FieldType = FieldType.Text;
typeOptions: any[];
cardBrandOptions: any[];
cardExpMonthOptions: any[];
identityTitleOptions: any[];
addFieldTypeOptions: any[];
uriMatchOptions: any[];
ownershipOptions: any[] = [];
autofillOnPageLoadOptions: any[];
currentDate = new Date();
allowPersonal = true;
reprompt: boolean = false;
protected writeableCollections: CollectionView[];
private previousCipherId: string;
constructor(protected cipherService: CipherService, protected folderService: FolderService,
protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService,
protected auditService: AuditService, protected stateService: StateService,
protected userService: UserService, protected collectionService: CollectionService,
protected messagingService: MessagingService, protected eventService: EventService,
protected policyService: PolicyService) {
this.typeOptions = [
{ name: i18nService.t('typeLogin'), value: CipherType.Login },
{ name: i18nService.t('typeCard'), value: CipherType.Card },
{ name: i18nService.t('typeIdentity'), value: CipherType.Identity },
{ name: i18nService.t('typeSecureNote'), value: CipherType.SecureNote },
];
this.cardBrandOptions = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: 'Visa', value: 'Visa' },
{ name: 'Mastercard', value: 'Mastercard' },
{ name: 'American Express', value: 'Amex' },
{ name: 'Discover', value: 'Discover' },
{ name: 'Diners Club', value: 'Diners Club' },
{ name: 'JCB', value: 'JCB' },
{ name: 'Maestro', value: 'Maestro' },
{ name: 'UnionPay', value: 'UnionPay' },
{ name: i18nService.t('other'), value: 'Other' },
];
this.cardExpMonthOptions = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: '01 - ' + i18nService.t('january'), value: '1' },
{ name: '02 - ' + i18nService.t('february'), value: '2' },
{ name: '03 - ' + i18nService.t('march'), value: '3' },
{ name: '04 - ' + i18nService.t('april'), value: '4' },
{ name: '05 - ' + i18nService.t('may'), value: '5' },
{ name: '06 - ' + i18nService.t('june'), value: '6' },
{ name: '07 - ' + i18nService.t('july'), value: '7' },
{ name: '08 - ' + i18nService.t('august'), value: '8' },
{ name: '09 - ' + i18nService.t('september'), value: '9' },
{ name: '10 - ' + i18nService.t('october'), value: '10' },
{ name: '11 - ' + i18nService.t('november'), value: '11' },
{ name: '12 - ' + i18nService.t('december'), value: '12' },
];
this.identityTitleOptions = [
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
{ name: i18nService.t('mr'), value: i18nService.t('mr') },
{ name: i18nService.t('mrs'), value: i18nService.t('mrs') },
{ name: i18nService.t('ms'), value: i18nService.t('ms') },
{ name: i18nService.t('dr'), value: i18nService.t('dr') },
];
this.addFieldTypeOptions = [
{ name: i18nService.t('cfTypeText'), value: FieldType.Text },
{ name: i18nService.t('cfTypeHidden'), value: FieldType.Hidden },
{ name: i18nService.t('cfTypeBoolean'), value: FieldType.Boolean },
];
this.uriMatchOptions = [
{ name: i18nService.t('defaultMatchDetection'), value: null },
{ name: i18nService.t('baseDomain'), value: UriMatchType.Domain },
{ name: i18nService.t('host'), value: UriMatchType.Host },
{ name: i18nService.t('startsWith'), value: UriMatchType.StartsWith },
{ name: i18nService.t('regEx'), value: UriMatchType.RegularExpression },
{ name: i18nService.t('exact'), value: UriMatchType.Exact },
{ name: i18nService.t('never'), value: UriMatchType.Never },
];
this.autofillOnPageLoadOptions = [
{ name: i18nService.t('autoFillOnPageLoadUseDefault'), value: null },
{ name: i18nService.t('autoFillOnPageLoadYes'), value: true },
{ name: i18nService.t('autoFillOnPageLoadNo'), value: false },
];
}
async ngOnInit() {
await this.init();
}
async init() {
const policies = await this.policyService.getAll(PolicyType.PersonalOwnership);
const myEmail = await this.userService.getEmail();
this.ownershipOptions.push({ name: myEmail, value: null });
const orgs = await this.userService.getAllOrganizations();
orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach(o => {
if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) {
this.ownershipOptions.push({ name: o.name, value: o.id });
if (policies != null && o.usePolicies && !o.canManagePolicies && this.allowPersonal) {
for (const policy of policies) {
if (policy.organizationId === o.id && policy.enabled) {
this.allowPersonal = false;
this.ownershipOptions.splice(0, 1);
// Default to the organization who owns this policy for now (if necessary)
if (this.organizationId == null) {
this.organizationId = o.id;
}
break;
}
}
}
}
});
this.writeableCollections = await this.loadCollections();
}
async load() {
this.editMode = this.cipherId != null;
if (this.editMode) {
this.editMode = true;
if (this.cloneMode) {
this.cloneMode = true;
this.title = this.i18nService.t('addItem');
} else {
this.title = this.i18nService.t('editItem');
}
} else {
this.title = this.i18nService.t('addItem');
}
const addEditCipherInfo: any = await this.stateService.get<any>('addEditCipherInfo');
if (addEditCipherInfo != null) {
this.cipher = addEditCipherInfo.cipher;
this.collectionIds = addEditCipherInfo.collectionIds;
}
await this.stateService.remove('addEditCipherInfo');
if (this.cipher == null) {
if (this.editMode) {
const cipher = await this.loadCipher();
this.cipher = await cipher.decrypt();
// Adjust Cipher Name if Cloning
if (this.cloneMode) {
this.cipher.name += ' - ' + this.i18nService.t('clone');
// If not allowing personal ownership, update cipher's org Id to prompt downstream changes
if (this.cipher.organizationId == null && !this.allowPersonal) {
this.cipher.organizationId = this.organizationId;
}
}
} else {
this.cipher = new CipherView();
this.cipher.organizationId = this.organizationId == null ? null : this.organizationId;
this.cipher.folderId = this.folderId;
this.cipher.type = this.type == null ? CipherType.Login : this.type;
this.cipher.login = new LoginView();
this.cipher.login.uris = [new LoginUriView()];
this.cipher.card = new CardView();
this.cipher.identity = new IdentityView();
this.cipher.secureNote = new SecureNoteView();
this.cipher.secureNote.type = SecureNoteType.Generic;
this.cipher.reprompt = CipherRepromptType.None;
}
}
if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) {
await this.organizationChanged();
if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) {
this.collections.forEach(c => {
if (this.collectionIds.indexOf(c.id) > -1) {
(c as any).checked = true;
}
});
}
}
this.folders = await this.folderService.getAllDecrypted();
if (this.editMode && this.previousCipherId !== this.cipherId) {
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
}
this.previousCipherId = this.cipherId;
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
}
async submit(): Promise<boolean> {
if (this.cipher.isDeleted) {
return this.restore();
}
if (this.cipher.name == null || this.cipher.name === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired'));
return false;
}
if ((!this.editMode || this.cloneMode) && !this.allowPersonal && this.cipher.organizationId == null) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('personalOwnershipSubmitError'));
return false;
}
if ((!this.editMode || this.cloneMode) && this.cipher.type === CipherType.Login &&
this.cipher.login.uris != null && this.cipher.login.uris.length === 1 &&
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) {
this.cipher.login.uris = null;
}
// Allows saving of selected collections during "Add" and "Clone" flows
if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) {
this.cipher.collectionIds = this.collections == null ? [] :
this.collections.filter(c => (c as any).checked).map(c => c.id);
}
// Clear current Cipher Id to trigger "Add" cipher flow
if (this.cloneMode) {
this.cipher.id = null;
}
const cipher = await this.encryptCipher();
try {
this.formPromise = this.saveCipher(cipher);
await this.formPromise;
this.cipher.id = cipher.id;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode && !this.cloneMode ? 'editedItem' : 'addedItem'));
this.onSavedCipher.emit(this.cipher);
this.messagingService.send(this.editMode && !this.cloneMode ? 'editedCipher' : 'addedCipher');
return true;
} catch { }
return false;
}
addUri() {
if (this.cipher.type !== CipherType.Login) {
return;
}
if (this.cipher.login.uris == null) {
this.cipher.login.uris = [];
}
this.cipher.login.uris.push(new LoginUriView());
}
removeUri(uri: LoginUriView) {
if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) {
return;
}
const i = this.cipher.login.uris.indexOf(uri);
if (i > -1) {
this.cipher.login.uris.splice(i, 1);
}
}
addField() {
if (this.cipher.fields == null) {
this.cipher.fields = [];
}
const f = new FieldView();
f.type = this.addFieldType;
f.newField = true;
this.cipher.fields.push(f);
}
removeField(field: FieldView) {
const i = this.cipher.fields.indexOf(field);
if (i > -1) {
this.cipher.fields.splice(i, 1);
}
}
trackByFunction(index: number, item: any) {
return index;
}
cancel() {
this.onCancelled.emit(this.cipher);
}
attachments() {
this.onEditAttachments.emit(this.cipher);
}
share() {
this.onShareCipher.emit(this.cipher);
}
editCollections() {
this.onEditCollections.emit(this.cipher);
}
async delete(): Promise<boolean> {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'),
this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.deleteCipher();
await this.deletePromise;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem'));
this.onDeletedCipher.emit(this.cipher);
this.messagingService.send(this.cipher.isDeleted ? 'permanentlyDeletedCipher' : 'deletedCipher');
} catch { }
return true;
}
async restore(): Promise<boolean> {
if (!this.cipher.isDeleted) {
return false;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.restorePromise = this.restoreCipher();
await this.restorePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem'));
this.onRestoredCipher.emit(this.cipher);
this.messagingService.send('restoredCipher');
} catch { }
return true;
}
async generatePassword(): Promise<boolean> {
if (this.cipher.login != null && this.cipher.login.password != null && this.cipher.login.password.length) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('overwritePasswordConfirmation'), this.i18nService.t('overwritePassword'),
this.i18nService.t('yes'), this.i18nService.t('no'));
if (!confirmed) {
return false;
}
}
this.onGeneratePassword.emit();
return true;
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById('loginPassword').focus();
if (this.editMode && this.showPassword) {
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
}
}
async toggleCardNumber() {
this.showCardNumber = !this.showCardNumber;
if (this.showCardNumber) {
this.eventService.collect(EventType.Cipher_ClientToggledCardNumberVisible, this.cipherId);
}
}
toggleCardCode() {
this.showCardCode = !this.showCardCode;
document.getElementById('cardCode').focus();
if (this.editMode && this.showCardCode) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
}
}
toggleFieldValue(field: FieldView) {
const f = (field as any);
f.showValue = !f.showValue;
if (this.editMode && f.showValue) {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
}
}
toggleUriOptions(uri: LoginUriView) {
const u = (uri as any);
u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions;
}
loginUriMatchChanged(uri: LoginUriView) {
const u = (uri as any);
u.showOptions = u.showOptions == null ? true : u.showOptions;
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.cipher.fields, event.previousIndex, event.currentIndex);
}
async organizationChanged() {
if (this.writeableCollections != null) {
this.writeableCollections.forEach(c => (c as any).checked = false);
}
if (this.cipher.organizationId != null) {
this.collections = this.writeableCollections.filter(c => c.organizationId === this.cipher.organizationId);
const org = await this.userService.getOrganization(this.cipher.organizationId);
if (org != null) {
this.cipher.organizationUseTotp = org.useTotp;
}
} else {
this.collections = [];
}
}
async checkPassword() {
if (this.checkPasswordPromise != null) {
return;
}
if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') {
return;
}
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
const matches = await this.checkPasswordPromise;
this.checkPasswordPromise = null;
if (matches > 0) {
this.platformUtilsService.showToast('warning', null,
this.i18nService.t('passwordExposed', matches.toString()));
} else {
this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordSafe'));
}
}
repromptChanged() {
this.reprompt = !this.reprompt;
if (this.reprompt) {
this.cipher.reprompt = CipherRepromptType.Password;
} else {
this.cipher.reprompt = CipherRepromptType.None;
}
}
protected async loadCollections() {
const allCollections = await this.collectionService.getAllDecrypted();
return allCollections.filter(c => !c.readOnly);
}
protected loadCipher() {
return this.cipherService.get(this.cipherId);
}
protected encryptCipher() {
return this.cipherService.encrypt(this.cipher);
}
protected saveCipher(cipher: Cipher) {
return this.cipherService.saveWithServer(cipher);
}
protected deleteCipher() {
return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id)
: this.cipherService.softDeleteWithServer(this.cipher.id);
}
protected restoreCipher() {
return this.cipherService.restoreWithServer(this.cipher.id);
}
}

View File

@@ -0,0 +1,243 @@
import {
Directive,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { Cipher } from 'jslib-common/models/domain/cipher';
import { ErrorResponse } from 'jslib-common/models/response';
import { AttachmentView } from 'jslib-common/models/view/attachmentView';
import { CipherView } from 'jslib-common/models/view/cipherView';
@Directive()
export class AttachmentsComponent implements OnInit {
@Input() cipherId: string;
@Output() onUploadedAttachment = new EventEmitter();
@Output() onDeletedAttachment = new EventEmitter();
@Output() onReuploadedAttachment = new EventEmitter();
cipher: CipherView;
cipherDomain: Cipher;
hasUpdatedKey: boolean;
canAccessAttachments: boolean;
formPromise: Promise<any>;
deletePromises: { [id: string]: Promise<any>; } = {};
reuploadPromises: { [id: string]: Promise<any>; } = {};
emergencyAccessId?: string = null;
constructor(protected cipherService: CipherService, protected i18nService: I18nService,
protected cryptoService: CryptoService, protected userService: UserService,
protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService,
protected win: Window) { }
async ngOnInit() {
await this.init();
}
async submit() {
if (!this.hasUpdatedKey) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('updateKey'));
return;
}
const fileEl = document.getElementById('file') as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectFile'));
return;
}
if (files[0].size > 524288000) { // 500 MB
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('maxFileSize'));
return;
}
try {
this.formPromise = this.saveCipherAttachment(files[0]);
this.cipherDomain = await this.formPromise;
this.cipher = await this.cipherDomain.decrypt();
this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved'));
this.onUploadedAttachment.emit();
} catch { }
// reset file input
// ref: https://stackoverflow.com/a/20552042
fileEl.type = '';
fileEl.type = 'file';
fileEl.value = '';
}
async delete(attachment: AttachmentView) {
if (this.deletePromises[attachment.id] != null) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteAttachmentConfirmation'), this.i18nService.t('deleteAttachment'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return;
}
try {
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
await this.deletePromises[attachment.id];
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedAttachment'));
const i = this.cipher.attachments.indexOf(attachment);
if (i > -1) {
this.cipher.attachments.splice(i, 1);
}
} catch { }
this.deletePromises[attachment.id] = null;
this.onDeletedAttachment.emit();
}
async download(attachment: AttachmentView) {
const a = (attachment as any);
if (a.downloading) {
return;
}
if (!this.canAccessAttachments) {
this.platformUtilsService.showToast('error', this.i18nService.t('premiumRequired'),
this.i18nService.t('premiumRequiredDesc'));
return;
}
let url: string;
try {
const attachmentDownloadResponse = await this.apiService.getAttachmentData(this.cipher.id, attachment.id,
this.emergencyAccessId);
url = attachmentDownloadResponse.url;
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = attachment.url;
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
a.downloading = true;
const response = await fetch(new Request(url, { cache: 'no-store' }));
if (response.status !== 200) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
a.downloading = false;
return;
}
try {
const buf = await response.arrayBuffer();
const key = attachment.key != null ? attachment.key :
await this.cryptoService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName);
} catch (e) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
a.downloading = false;
}
protected async init() {
this.cipherDomain = await this.loadCipher();
this.cipher = await this.cipherDomain.decrypt();
this.hasUpdatedKey = await this.cryptoService.hasEncKey();
const canAccessPremium = await this.userService.canAccessPremium();
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
if (!this.canAccessAttachments) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (confirmed) {
this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase');
}
} else if (!this.hasUpdatedKey) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('updateKey'), this.i18nService.t('featureUnavailable'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'), 'warning');
if (confirmed) {
this.platformUtilsService.launchUri('https://help.bitwarden.com/article/update-encryption-key/');
}
}
}
protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) {
const a = (attachment as any);
if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) {
return;
}
try {
this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => {
// 1. Download
a.downloading = true;
const response = await fetch(new Request(attachment.url, { cache: 'no-store' }));
if (response.status !== 200) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
a.downloading = false;
return;
}
try {
// 2. Resave
const buf = await response.arrayBuffer();
const key = attachment.key != null ? attachment.key :
await this.cryptoService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
this.cipherDomain, attachment.fileName, decBuf, admin);
this.cipher = await this.cipherDomain.decrypt();
// 3. Delete old
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
await this.deletePromises[attachment.id];
const foundAttachment = this.cipher.attachments.filter(a2 => a2.id === attachment.id);
if (foundAttachment.length > 0) {
const i = this.cipher.attachments.indexOf(foundAttachment[0]);
if (i > -1) {
this.cipher.attachments.splice(i, 1);
}
}
this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved'));
this.onReuploadedAttachment.emit();
} catch (e) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
a.downloading = false;
});
await this.reuploadPromises[attachment.id];
} catch { }
}
protected loadCipher() {
return this.cipherService.get(this.cipherId);
}
protected saveCipherAttachment(file: File) {
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file);
}
protected deleteCipherAttachment(attachmentId: string) {
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId);
}
}

View File

@@ -0,0 +1,7 @@
<div class="callout callout-{{calloutStyle}}" [ngClass]="{'clickable': clickable}" role="alert">
<h3 class="callout-heading" *ngIf="title">
<i class="fa {{icon}}" *ngIf="icon" aria-hidden="true"></i>
{{title}}
</h3>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,54 @@
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
@Component({
selector: 'app-callout',
templateUrl: 'callout.component.html',
})
export class CalloutComponent implements OnInit {
@Input() type = 'info';
@Input() icon: string;
@Input() title: string;
@Input() clickable: boolean;
calloutStyle: string;
constructor(private i18nService: I18nService) { }
ngOnInit() {
this.calloutStyle = this.type;
if (this.type === 'warning' || this.type === 'danger') {
if (this.type === 'danger') {
this.calloutStyle = 'danger';
}
if (this.title === undefined) {
this.title = this.i18nService.t('warning');
}
if (this.icon === undefined) {
this.icon = 'fa-warning';
}
} else if (this.type === 'error') {
this.calloutStyle = 'danger';
if (this.title === undefined) {
this.title = this.i18nService.t('error');
}
if (this.icon === undefined) {
this.icon = 'fa-bolt';
}
} else if (this.type === 'tip') {
this.calloutStyle = 'success';
if (this.title === undefined) {
this.title = this.i18nService.t('tip');
}
if (this.icon === undefined) {
this.icon = 'fa-lightbulb-o';
}
}
}
}

View File

@@ -0,0 +1,172 @@
import { Directive, OnInit } from '@angular/core';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { EncString } from 'jslib-common/models/domain/encString';
import { MasterPasswordPolicyOptions } from 'jslib-common/models/domain/masterPasswordPolicyOptions';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { KdfType } from 'jslib-common/enums/kdfType';
@Directive()
export class ChangePasswordComponent implements OnInit {
masterPassword: string;
masterPasswordRetype: string;
formPromise: Promise<any>;
masterPasswordScore: number;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
protected email: string;
protected kdf: KdfType;
protected kdfIterations: number;
private masterPasswordStrengthTimeout: any;
constructor(protected i18nService: I18nService, protected cryptoService: CryptoService,
protected messagingService: MessagingService, protected userService: UserService,
protected passwordGenerationService: PasswordGenerationService,
protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService) { }
async ngOnInit() {
this.email = await this.userService.getEmail();
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
}
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return '';
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t('strong');
break;
case 3:
str = this.i18nService.t('good');
break;
default:
str = this.i18nService.t('weak');
break;
}
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
}
async submit() {
if (!await this.strongPassword()) {
return;
}
if (!await this.setupSubmitActions()) {
return;
}
const email = await this.userService.getEmail();
if (this.kdf == null) {
this.kdf = await this.userService.getKdf();
}
if (this.kdfIterations == null) {
this.kdfIterations = await this.userService.getKdfIterations();
}
const key = await this.cryptoService.makeKey(this.masterPassword, email.trim().toLowerCase(),
this.kdf, this.kdfIterations);
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
let encKey: [SymmetricCryptoKey, EncString] = null;
const existingEncKey = await this.cryptoService.getEncKey();
if (existingEncKey == null) {
encKey = await this.cryptoService.makeEncKey(key);
} else {
encKey = await this.cryptoService.remakeEncKey(key);
}
await this.performSubmitActions(masterPasswordHash, key, encKey);
}
async setupSubmitActions(): Promise<boolean> {
// Override in sub-class
// Can be used for additional validation and/or other processes the should occur before changing passwords
return true;
}
async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey,
encKey: [SymmetricCryptoKey, EncString]) {
// Override in sub-class
}
async strongPassword(): Promise<boolean> {
if (this.masterPassword == null || this.masterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return false;
}
if (this.masterPassword.length < 8) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassLength'));
return false;
}
if (this.masterPassword !== this.masterPasswordRetype) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassDoesntMatch'));
return false;
}
const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword,
this.getPasswordStrengthUserInput());
if (this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
this.masterPassword,
this.enforcedPolicyOptions)) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
return false;
}
if (strengthResult != null && strengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
'warning');
if (!result) {
return false;
}
}
return true;
}
updatePasswordStrength() {
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
this.masterPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword,
this.getPasswordStrengthUserInput());
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'),
this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel'));
if (confirmed) {
this.messagingService.send('logout');
}
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf('@');
if (atPosition > -1) {
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
}
return userInput;
}
}

View File

@@ -0,0 +1,131 @@
import {
Directive,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { CipherView } from 'jslib-common/models/view/cipherView';
@Directive()
export class CiphersComponent {
@Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
@Output() onAddCipher = new EventEmitter();
@Output() onAddCipherOptions = new EventEmitter();
loaded: boolean = false;
ciphers: CipherView[] = [];
pagedCiphers: CipherView[] = [];
searchText: string;
searchPlaceholder: string = null;
filter: (cipher: CipherView) => boolean = null;
deleted: boolean = false;
protected searchPending = false;
protected didScroll = false;
protected pageSize = 100;
private searchTimeout: any = null;
private pagedCiphersCount = 0;
private refreshing = false;
constructor(protected searchService: SearchService) { }
async load(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) {
this.deleted = deleted || false;
await this.applyFilter(filter);
this.loaded = true;
}
loadMore() {
if (this.ciphers.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedCiphers.length;
let pagedSize = this.pageSize;
if (this.refreshing && pagedLength === 0 && this.pagedCiphersCount > this.pageSize) {
pagedSize = this.pagedCiphersCount;
}
if (this.ciphers.length > pagedLength) {
this.pagedCiphers = this.pagedCiphers.concat(this.ciphers.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedCiphersCount = this.pagedCiphers.length;
this.didScroll = this.pagedCiphers.length > this.pageSize;
}
async reload(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) {
this.loaded = false;
this.ciphers = [];
await this.load(filter, deleted);
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter, this.deleted);
} finally {
this.refreshing = false;
}
}
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
this.filter = filter;
await this.search(null);
}
async search(timeout: number = null, indexedCiphers?: CipherView[]) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
const deletedFilter: (cipher: CipherView) => boolean = c => c.isDeleted === this.deleted;
if (timeout == null) {
this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], indexedCiphers);
await this.resetPaging();
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], indexedCiphers);
await this.resetPaging();
this.searchPending = false;
}, timeout);
}
selectCipher(cipher: CipherView) {
this.onCipherClicked.emit(cipher);
}
rightClickCipher(cipher: CipherView) {
this.onCipherRightClicked.emit(cipher);
}
addCipher() {
this.onAddCipher.emit();
}
addCipherOptions() {
this.onAddCipherOptions.emit();
}
isSearching() {
return !this.searchPending && this.searchService.isSearchable(this.searchText);
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.ciphers.length > this.pageSize;
}
async resetPaging() {
this.pagedCiphers = [];
this.loadMore();
}
}

View File

@@ -0,0 +1,87 @@
import {
Directive,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { Cipher } from 'jslib-common/models/domain/cipher';
@Directive()
export class CollectionsComponent implements OnInit {
@Input() cipherId: string;
@Input() allowSelectNone = false;
@Output() onSavedCollections = new EventEmitter();
formPromise: Promise<any>;
cipher: CipherView;
collectionIds: string[];
collections: CollectionView[] = [];
protected cipherDomain: Cipher;
constructor(protected collectionService: CollectionService, protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService, protected cipherService: CipherService) { }
async ngOnInit() {
await this.load();
}
async load() {
this.cipherDomain = await this.loadCipher();
this.collectionIds = this.loadCipherCollections();
this.cipher = await this.cipherDomain.decrypt();
this.collections = await this.loadCollections();
this.collections.forEach(c => (c as any).checked = false);
if (this.collectionIds != null) {
this.collections.forEach(c => {
(c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1;
});
}
}
async submit() {
const selectedCollectionIds = this.collections
.filter(c => !!(c as any).checked)
.map(c => c.id);
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectOneCollection'));
return;
}
this.cipherDomain.collectionIds = selectedCollectionIds;
try {
this.formPromise = this.saveCollections();
await this.formPromise;
this.onSavedCollections.emit();
this.platformUtilsService.showToast('success', null, this.i18nService.t('editedItem'));
} catch { }
}
protected loadCipher() {
return this.cipherService.get(this.cipherId);
}
protected loadCipherCollections() {
return this.cipherDomain.collectionIds;
}
protected async loadCollections() {
const allCollections = await this.collectionService.getAllDecrypted();
return allCollections.filter(c => !c.readOnly && c.organizationId === this.cipher.organizationId);
}
protected saveCollections() {
return this.cipherService.saveCollectionsWithServer(this.cipherDomain);
}
}

View File

@@ -0,0 +1,66 @@
import {
Directive,
EventEmitter,
Output,
} from '@angular/core';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
@Directive()
export class EnvironmentComponent {
@Output() onSaved = new EventEmitter();
iconsUrl: string;
identityUrl: string;
apiUrl: string;
webVaultUrl: string;
notificationsUrl: string;
baseUrl: string;
showCustom = false;
enterpriseUrl: string;
constructor(protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService,
protected i18nService: I18nService) {
this.baseUrl = environmentService.baseUrl || '';
this.webVaultUrl = environmentService.webVaultUrl || '';
this.apiUrl = environmentService.apiUrl || '';
this.identityUrl = environmentService.identityUrl || '';
this.iconsUrl = environmentService.iconsUrl || '';
this.notificationsUrl = environmentService.notificationsUrl || '';
this.enterpriseUrl = environmentService.enterpriseUrl || '';
}
async submit() {
const resUrls = await this.environmentService.setUrls({
base: this.baseUrl,
api: this.apiUrl,
identity: this.identityUrl,
webVault: this.webVaultUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
enterprise: this.enterpriseUrl,
});
// re-set urls since service can change them, ex: prefixing https://
this.baseUrl = resUrls.base;
this.apiUrl = resUrls.api;
this.identityUrl = resUrls.identity;
this.webVaultUrl = resUrls.webVault;
this.iconsUrl = resUrls.icons;
this.notificationsUrl = resUrls.notifications;
this.enterpriseUrl = resUrls.enterprise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('environmentSaved'));
this.saved();
}
toggleCustom() {
this.showCustom = !this.showCustom;
}
protected saved() {
this.onSaved.emit();
}
}

View File

@@ -0,0 +1,109 @@
import {
Directive,
EventEmitter,
Output,
} from '@angular/core';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EventService } from 'jslib-common/abstractions/event.service';
import { ExportService } from 'jslib-common/abstractions/export.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { EventType } from 'jslib-common/enums/eventType';
@Directive()
export class ExportComponent {
@Output() onSaved = new EventEmitter();
formPromise: Promise<string>;
masterPassword: string;
format: 'json' | 'encrypted_json' | 'csv' = 'json';
showPassword = false;
constructor(protected cryptoService: CryptoService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService,
protected eventService: EventService, protected win: Window) { }
get encryptedFormat() {
return this.format === 'encrypted_json';
}
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidMasterPassword'));
return;
}
const acceptedWarning = await this.warningDialog();
if (!acceptedWarning) {
return;
}
const keyHash = await this.cryptoService.hashPassword(this.masterPassword, null);
const storedKeyHash = await this.cryptoService.getKeyHash();
if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) {
try {
this.formPromise = this.getExportData();
const data = await this.formPromise;
this.downloadFile(data);
this.saved();
await this.collectEvent();
} catch { }
} else {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidMasterPassword'));
}
}
async warningDialog() {
if (this.encryptedFormat) {
return await this.platformUtilsService.showDialog(
'<p>' + this.i18nService.t('encExportKeyWarningDesc') +
'<p>' + this.i18nService.t('encExportAccountWarningDesc'),
this.i18nService.t('confirmVaultExport'), this.i18nService.t('exportVault'),
this.i18nService.t('cancel'), 'warning',
true);
} else {
return await this.platformUtilsService.showDialog(
this.i18nService.t('exportWarningDesc'),
this.i18nService.t('confirmVaultExport'), this.i18nService.t('exportVault'),
this.i18nService.t('cancel'), 'warning');
}
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById('masterPassword').focus();
}
protected saved() {
this.onSaved.emit();
}
protected getExportData() {
return this.exportService.getExport(this.format);
}
protected getFileName(prefix?: string) {
let extension = this.format;
if (this.format === 'encrypted_json') {
if (prefix == null) {
prefix = 'encrypted';
} else {
prefix = 'encrypted_' + prefix;
}
extension = 'json';
}
return this.exportService.getFileName(prefix, extension);
}
protected async collectEvent(): Promise<any> {
await this.eventService.collect(EventType.User_ClientExportedVault);
}
private downloadFile(csv: string): void {
const fileName = this.getFileName();
this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName);
}
}

View File

@@ -0,0 +1,84 @@
import {
Directive,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { FolderView } from 'jslib-common/models/view/folderView';
@Directive()
export class FolderAddEditComponent implements OnInit {
@Input() folderId: string;
@Output() onSavedFolder = new EventEmitter<FolderView>();
@Output() onDeletedFolder = new EventEmitter<FolderView>();
editMode: boolean = false;
folder: FolderView = new FolderView();
title: string;
formPromise: Promise<any>;
deletePromise: Promise<any>;
constructor(protected folderService: FolderService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService) { }
async ngOnInit() {
await this.init();
}
async submit(): Promise<boolean> {
if (this.folder.name == null || this.folder.name === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired'));
return false;
}
try {
const folder = await this.folderService.encrypt(this.folder);
this.formPromise = this.folderService.saveWithServer(folder);
await this.formPromise;
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode ? 'editedFolder' : 'addedFolder'));
this.onSavedFolder.emit(this.folder);
return true;
} catch { }
return false;
}
async delete(): Promise<boolean> {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteFolderConfirmation'), this.i18nService.t('deleteFolder'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.folderService.deleteWithServer(this.folder.id);
await this.deletePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedFolder'));
this.onDeletedFolder.emit(this.folder);
} catch { }
return true;
}
protected async init() {
this.editMode = this.folderId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editFolder');
const folder = await this.folderService.get(this.folderId);
this.folder = await folder.decrypt();
} else {
this.title = this.i18nService.t('addFolder');
}
}
}

View File

@@ -0,0 +1,168 @@
import {
Directive,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { CipherType } from 'jslib-common/enums/cipherType';
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { FolderView } from 'jslib-common/models/view/folderView';
import { TreeNode } from 'jslib-common/models/domain/treeNode';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
@Directive()
export class GroupingsComponent {
@Input() showFolders = true;
@Input() showCollections = true;
@Input() showFavorites = true;
@Input() showTrash = true;
@Output() onAllClicked = new EventEmitter();
@Output() onFavoritesClicked = new EventEmitter();
@Output() onTrashClicked = new EventEmitter();
@Output() onCipherTypeClicked = new EventEmitter<CipherType>();
@Output() onFolderClicked = new EventEmitter<FolderView>();
@Output() onAddFolder = new EventEmitter();
@Output() onEditFolder = new EventEmitter<FolderView>();
@Output() onCollectionClicked = new EventEmitter<CollectionView>();
folders: FolderView[];
nestedFolders: TreeNode<FolderView>[];
collections: CollectionView[];
nestedCollections: TreeNode<CollectionView>[];
loaded: boolean = false;
cipherType = CipherType;
selectedAll: boolean = false;
selectedFavorites: boolean = false;
selectedTrash: boolean = false;
selectedType: CipherType = null;
selectedFolder: boolean = false;
selectedFolderId: string = null;
selectedCollectionId: string = null;
private collapsedGroupings: Set<string>;
private collapsedGroupingsKey: string;
constructor(protected collectionService: CollectionService, protected folderService: FolderService,
protected storageService: StorageService, protected userService: UserService) { }
async load(setLoaded = true) {
const userId = await this.userService.getUserId();
this.collapsedGroupingsKey = ConstantsService.collapsedGroupingsKey + '_' + userId;
const collapsedGroupings = await this.storageService.get<string[]>(this.collapsedGroupingsKey);
if (collapsedGroupings == null) {
this.collapsedGroupings = new Set<string>();
} else {
this.collapsedGroupings = new Set(collapsedGroupings);
}
await this.loadFolders();
await this.loadCollections();
if (setLoaded) {
this.loaded = true;
}
}
async loadCollections(organizationId?: string) {
if (!this.showCollections) {
return;
}
const collections = await this.collectionService.getAllDecrypted();
if (organizationId != null) {
this.collections = collections.filter(c => c.organizationId === organizationId);
} else {
this.collections = collections;
}
this.nestedCollections = await this.collectionService.getAllNested(this.collections);
}
async loadFolders() {
if (!this.showFolders) {
return;
}
this.folders = await this.folderService.getAllDecrypted();
this.nestedFolders = await this.folderService.getAllNested();
}
selectAll() {
this.clearSelections();
this.selectedAll = true;
this.onAllClicked.emit();
}
selectFavorites() {
this.clearSelections();
this.selectedFavorites = true;
this.onFavoritesClicked.emit();
}
selectTrash() {
this.clearSelections();
this.selectedTrash = true;
this.onTrashClicked.emit();
}
selectType(type: CipherType) {
this.clearSelections();
this.selectedType = type;
this.onCipherTypeClicked.emit(type);
}
selectFolder(folder: FolderView) {
this.clearSelections();
this.selectedFolder = true;
this.selectedFolderId = folder.id;
this.onFolderClicked.emit(folder);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
selectCollection(collection: CollectionView) {
this.clearSelections();
this.selectedCollectionId = collection.id;
this.onCollectionClicked.emit(collection);
}
clearSelections() {
this.selectedAll = false;
this.selectedFavorites = false;
this.selectedTrash = false;
this.selectedType = null;
this.selectedFolder = false;
this.selectedFolderId = null;
this.selectedCollectionId = null;
}
collapse(grouping: FolderView | CollectionView, idPrefix = '') {
if (grouping.id == null) {
return;
}
const id = idPrefix + grouping.id;
if (this.isCollapsed(grouping, idPrefix)) {
this.collapsedGroupings.delete(id);
} else {
this.collapsedGroupings.add(id);
}
this.storageService.save(this.collapsedGroupingsKey, this.collapsedGroupings);
}
isCollapsed(grouping: FolderView | CollectionView, idPrefix = '') {
return this.collapsedGroupings.has(idPrefix + grouping.id);
}
}

View File

@@ -0,0 +1,42 @@
import { Router } from '@angular/router';
import { PasswordHintRequest } from 'jslib-common/models/request/passwordHintRequest';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
export class HintComponent {
email: string = '';
formPromise: Promise<any>;
protected successRoute = 'login';
protected onSuccessfulSubmit: () => void;
constructor(protected router: Router, protected i18nService: I18nService,
protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService) { }
async submit() {
if (this.email == null || this.email === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('emailRequired'));
return;
}
if (this.email.indexOf('@') === -1) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidEmail'));
return;
}
try {
this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('masterPassSent'));
if (this.onSuccessfulSubmit != null) {
this.onSuccessfulSubmit();
} else if (this.router != null) {
this.router.navigate([this.successRoute]);
}
} catch { }
}
}

View File

@@ -0,0 +1,4 @@
<div class="icon" aria-hidden="true">
<img [src]="image" appFallbackSrc="{{fallbackImage}}" *ngIf="imageEnabled && image" alt="" />
<i class="fa fa-fw fa-lg {{icon}}" *ngIf="!imageEnabled || !image"></i>
</div>

View File

@@ -0,0 +1,108 @@
import {
Component,
Input,
OnChanges,
} from '@angular/core';
import { CipherType } from 'jslib-common/enums/cipherType';
import { CipherView } from 'jslib-common/models/view/cipherView';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { Utils } from 'jslib-common/misc/utils';
const IconMap: any = {
'fa-globe': String.fromCharCode(0xf0ac),
'fa-sticky-note-o': String.fromCharCode(0xf24a),
'fa-id-card-o': String.fromCharCode(0xf2c3),
'fa-credit-card': String.fromCharCode(0xf09d),
'fa-android': String.fromCharCode(0xf17b),
'fa-apple': String.fromCharCode(0xf179),
};
@Component({
selector: 'app-vault-icon',
templateUrl: 'icon.component.html',
})
export class IconComponent implements OnChanges {
@Input() cipher: CipherView;
icon: string;
image: string;
fallbackImage: string;
imageEnabled: boolean;
private iconsUrl: string;
constructor(environmentService: EnvironmentService, protected stateService: StateService) {
this.iconsUrl = environmentService.iconsUrl;
if (!this.iconsUrl) {
if (environmentService.baseUrl) {
this.iconsUrl = environmentService.baseUrl + '/icons';
} else {
this.iconsUrl = 'https://icons.bitwarden.net';
}
}
}
async ngOnChanges() {
this.imageEnabled = !(await this.stateService.get<boolean>(ConstantsService.disableFaviconKey));
this.load();
}
get iconCode(): string {
return IconMap[this.icon];
}
protected load() {
switch (this.cipher.type) {
case CipherType.Login:
this.icon = 'fa-globe';
this.setLoginIcon();
break;
case CipherType.SecureNote:
this.icon = 'fa-sticky-note-o';
break;
case CipherType.Card:
this.icon = 'fa-credit-card';
break;
case CipherType.Identity:
this.icon = 'fa-id-card-o';
break;
default:
break;
}
}
private setLoginIcon() {
if (this.cipher.login.uri) {
let hostnameUri = this.cipher.login.uri;
let isWebsite = false;
if (hostnameUri.indexOf('androidapp://') === 0) {
this.icon = 'fa-android';
this.image = null;
} else if (hostnameUri.indexOf('iosapp://') === 0) {
this.icon = 'fa-apple';
this.image = null;
} else if (this.imageEnabled && hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) {
hostnameUri = 'http://' + hostnameUri;
isWebsite = true;
} else if (this.imageEnabled) {
isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1;
}
if (this.imageEnabled && isWebsite) {
try {
this.image = this.iconsUrl + '/' + Utils.getHostname(hostnameUri) + '/icon.png';
this.fallbackImage = 'images/fa-globe.png';
} catch (e) { }
}
} else {
this.image = null;
}
}
}

View File

@@ -0,0 +1,188 @@
import { Directive, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { EncString } from 'jslib-common/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { PasswordVerificationRequest } from 'jslib-common/models/request/passwordVerificationRequest';
import { Utils } from 'jslib-common/misc/utils';
@Directive()
export class LockComponent implements OnInit {
masterPassword: string = '';
pin: string = '';
showPassword: boolean = false;
email: string;
pinLock: boolean = false;
webVaultHostname: string = '';
formPromise: Promise<any>;
supportsBiometric: boolean;
biometricLock: boolean;
biometricText: string;
protected successRoute: string = 'vault';
protected onSuccessfulSubmit: () => void;
private invalidPinAttempts = 0;
private pinSet: [boolean, boolean];
constructor(protected router: Router, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService,
protected userService: UserService, protected cryptoService: CryptoService,
protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService,
protected environmentService: EnvironmentService, protected stateService: StateService,
protected apiService: ApiService) { }
async ngOnInit() {
this.pinSet = await this.vaultTimeoutService.isPinLockSet();
this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1];
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && (await this.cryptoService.hasKey() || !this.platformUtilsService.supportsSecureStorage());
this.biometricText = await this.storageService.get(ConstantsService.biometricText);
this.email = await this.userService.getEmail();
let vaultUrl = this.environmentService.getWebVaultUrl();
if (vaultUrl == null) {
vaultUrl = 'https://bitwarden.com';
}
this.webVaultHostname = Utils.getHostname(vaultUrl);
}
async submit() {
if (this.pinLock && (this.pin == null || this.pin === '')) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('pinRequired'));
return;
}
if (!this.pinLock && (this.masterPassword == null || this.masterPassword === '')) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
const kdf = await this.userService.getKdf();
const kdfIterations = await this.userService.getKdfIterations();
if (this.pinLock) {
let failed = true;
try {
if (this.pinSet[0]) {
const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations,
this.vaultTimeoutService.pinProtectedKey);
const encKey = await this.cryptoService.getEncKey(key);
const protectedPin = await this.storageService.get<string>(ConstantsService.protectedPin);
const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey);
failed = decPin !== this.pin;
if (!failed) {
await this.setKeyAndContinue(key);
}
} else {
const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations);
failed = false;
await this.setKeyAndContinue(key);
}
} catch {
failed = true;
}
if (failed) {
this.invalidPinAttempts++;
if (this.invalidPinAttempts >= 5) {
this.messagingService.send('logout');
return;
}
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidPin'));
}
} else {
const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations);
const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key);
let passwordValid = false;
if (keyHash != null) {
const storedKeyHash = await this.cryptoService.getKeyHash();
if (storedKeyHash != null) {
passwordValid = storedKeyHash === keyHash;
} else {
const request = new PasswordVerificationRequest();
request.masterPasswordHash = keyHash;
try {
this.formPromise = this.apiService.postAccountVerifyPassword(request);
await this.formPromise;
passwordValid = true;
await this.cryptoService.setKeyHash(keyHash);
} catch { }
}
}
if (passwordValid) {
if (this.pinSet[0]) {
const protectedPin = await this.storageService.get<string>(ConstantsService.protectedPin);
const encKey = await this.cryptoService.getEncKey(key);
const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey);
const pinKey = await this.cryptoService.makePinKey(decPin, this.email, kdf, kdfIterations);
this.vaultTimeoutService.pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey);
}
this.setKeyAndContinue(key);
} else {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidMasterPassword'));
}
}
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'),
this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel'));
if (confirmed) {
this.messagingService.send('logout');
}
}
async unlockBiometric() {
if (!this.biometricLock) {
return;
}
const success = await this.platformUtilsService.authenticateBiometric();
if (success) {
await this.doContinue();
}
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById(this.pinLock ? 'pin' : 'masterPassword').focus();
}
private async setKeyAndContinue(key: SymmetricCryptoKey) {
await this.cryptoService.setKey(key);
this.doContinue();
}
private async doContinue() {
this.vaultTimeoutService.biometricLocked = false;
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
this.messagingService.send('unlocked');
if (this.onSuccessfulSubmit != null) {
this.onSuccessfulSubmit();
} else if (this.router != null) {
this.router.navigate([this.successRoute]);
}
}
}

View File

@@ -0,0 +1,146 @@
import {
Directive,
Input,
OnInit,
} from '@angular/core';
import { Router } from '@angular/router';
import { AuthResult } from 'jslib-common/models/domain/authResult';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { Utils } from 'jslib-common/misc/utils';
const Keys = {
rememberedEmail: 'rememberedEmail',
rememberEmail: 'rememberEmail',
};
@Directive()
export class LoginComponent implements OnInit {
@Input() email: string = '';
@Input() rememberEmail = true;
masterPassword: string = '';
showPassword: boolean = false;
formPromise: Promise<AuthResult>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
protected twoFactorRoute = '2fa';
protected successRoute = 'vault';
constructor(protected authService: AuthService, protected router: Router,
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService,
protected stateService: StateService, protected environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationService,
protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService) { }
async ngOnInit() {
if (this.email == null || this.email === '') {
this.email = await this.storageService.get<string>(Keys.rememberedEmail);
if (this.email == null) {
this.email = '';
}
}
this.rememberEmail = await this.storageService.get<boolean>(Keys.rememberEmail);
if (this.rememberEmail == null) {
this.rememberEmail = true;
}
if (Utils.isBrowser) {
document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus();
}
}
async submit() {
if (this.email == null || this.email === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('emailRequired'));
return;
}
if (this.email.indexOf('@') === -1) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidEmail'));
return;
}
if (this.masterPassword == null || this.masterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
try {
this.formPromise = this.authService.logIn(this.email, this.masterPassword);
const response = await this.formPromise;
await this.storageService.save(Keys.rememberEmail, this.rememberEmail);
if (this.rememberEmail) {
await this.storageService.save(Keys.rememberedEmail, this.email);
} else {
await this.storageService.remove(Keys.rememberedEmail);
}
if (response.twoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
this.onSuccessfulLoginTwoFactorNavigate();
} else {
this.router.navigate([this.twoFactorRoute]);
}
} else {
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute]);
}
}
} catch { }
}
togglePassword() {
this.showPassword = !this.showPassword;
document.getElementById('masterPassword').focus();
}
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
// Generate necessary sso params
const passwordOptions: any = {
type: 'password',
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256');
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
// Save sso params
await this.storageService.save(ConstantsService.ssoStateKey, state);
await this.storageService.save(ConstantsService.ssoCodeVerifierKey, ssoCodeVerifier);
// Build URI
const webUrl = this.environmentService.getWebVaultUrl() == null ? 'https://vault.bitwarden.com' :
this.environmentService.getWebVaultUrl();
// Launch browser
this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + clientId +
'&redirectUri=' + encodeURIComponent(ssoRedirectUri) +
'&state=' + state + '&codeChallenge=' + codeChallenge);
}
}

View File

@@ -0,0 +1,78 @@
import {
Component,
ComponentFactoryResolver,
EventEmitter,
OnDestroy,
Output,
Type,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
@Component({
selector: 'app-modal',
template: `<ng-template #container></ng-template>`,
})
export class ModalComponent implements OnDestroy {
@Output() onClose = new EventEmitter();
@Output() onClosed = new EventEmitter();
@Output() onShow = new EventEmitter();
@Output() onShown = new EventEmitter();
@ViewChild('container', { read: ViewContainerRef, static: true }) container: ViewContainerRef;
parentContainer: ViewContainerRef = null;
fade: boolean = true;
constructor(protected componentFactoryResolver: ComponentFactoryResolver,
protected messagingService: MessagingService) { }
ngOnDestroy() {
document.body.classList.remove('modal-open');
document.body.removeChild(document.querySelector('.modal-backdrop'));
}
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true,
setComponentParameters: (component: T) => void = null): T {
this.onShow.emit();
this.messagingService.send('modalShow');
this.parentContainer = parentContainer;
this.fade = fade;
document.body.classList.add('modal-open');
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop' + (this.fade ? ' fade' : '');
document.body.appendChild(backdrop);
const factory = this.componentFactoryResolver.resolveComponentFactory<T>(type);
const componentRef = this.container.createComponent<T>(factory);
if (setComponentParameters != null) {
setComponentParameters(componentRef.instance);
}
document.querySelector('.modal-dialog').addEventListener('click', (e: Event) => {
e.stopPropagation();
});
const modals = Array.from(document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]'));
for (const closeElement of modals) {
closeElement.addEventListener('click', event => {
this.close();
});
}
this.onShown.emit();
this.messagingService.send('modalShown');
return componentRef.instance;
}
close() {
this.onClose.emit();
this.messagingService.send('modalClose');
this.onClosed.emit();
this.messagingService.send('modalClosed');
if (this.parentContainer != null) {
this.parentContainer.clear();
}
}
}

View File

@@ -0,0 +1,32 @@
import { Directive, OnInit } from '@angular/core';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { GeneratedPasswordHistory } from 'jslib-common/models/domain/generatedPasswordHistory';
@Directive()
export class PasswordGeneratorHistoryComponent implements OnInit {
history: GeneratedPasswordHistory[] = [];
constructor(protected passwordGenerationService: PasswordGenerationService,
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService,
private win: Window) { }
async ngOnInit() {
this.history = await this.passwordGenerationService.getHistory();
}
clear() {
this.history = [];
this.passwordGenerationService.clear();
}
copy(password: string) {
const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(password, copyOptions);
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t('password')));
}
}

View File

@@ -0,0 +1,95 @@
import {
Directive,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PasswordGeneratorPolicyOptions } from 'jslib-common/models/domain/passwordGeneratorPolicyOptions';
@Directive()
export class PasswordGeneratorComponent implements OnInit {
@Input() showSelect: boolean = false;
@Output() onSelected = new EventEmitter<string>();
options: any = {};
password: string = '-';
showOptions = false;
avoidAmbiguous = false;
enforcedPolicyOptions: PasswordGeneratorPolicyOptions;
constructor(protected passwordGenerationService: PasswordGenerationService,
protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService,
private win: Window) { }
async ngOnInit() {
const optionsResponse = await this.passwordGenerationService.getOptions();
this.options = optionsResponse[0];
this.enforcedPolicyOptions = optionsResponse[1];
this.avoidAmbiguous = !this.options.ambiguous;
this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password';
this.password = await this.passwordGenerationService.generatePassword(this.options);
await this.passwordGenerationService.addHistory(this.password);
}
async sliderChanged() {
this.saveOptions(false);
await this.passwordGenerationService.addHistory(this.password);
}
async sliderInput() {
this.normalizeOptions();
this.password = await this.passwordGenerationService.generatePassword(this.options);
}
async saveOptions(regenerate: boolean = true) {
this.normalizeOptions();
await this.passwordGenerationService.saveOptions(this.options);
if (regenerate) {
await this.regenerate();
}
}
async regenerate() {
this.password = await this.passwordGenerationService.generatePassword(this.options);
await this.passwordGenerationService.addHistory(this.password);
}
copy() {
const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(this.password, copyOptions);
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t('password')));
}
select() {
this.onSelected.emit(this.password);
}
toggleOptions() {
this.showOptions = !this.showOptions;
}
private normalizeOptions() {
// Application level normalize options depedent on class variables
this.options.ambiguous = !this.avoidAmbiguous;
if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) {
this.options.lowercase = true;
if (this.win != null) {
const lowercase = this.win.document.querySelector('#lowercase') as HTMLInputElement;
if (lowercase) {
lowercase.checked = true;
}
}
}
this.passwordGenerationService.normalizeOptions(this.options, this.enforcedPolicyOptions);
}
}

View File

@@ -0,0 +1,33 @@
import { Directive, OnInit } from '@angular/core';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PasswordHistoryView } from 'jslib-common/models/view/passwordHistoryView';
@Directive()
export class PasswordHistoryComponent implements OnInit {
cipherId: string;
history: PasswordHistoryView[] = [];
constructor(protected cipherService: CipherService, protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService, private win: Window) { }
async ngOnInit() {
await this.init();
}
copy(password: string) {
const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(password, copyOptions);
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t('password')));
}
protected async init() {
const cipher = await this.cipherService.get(this.cipherId);
const decCipher = await cipher.decrypt();
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
}
}

View File

@@ -0,0 +1,46 @@
import { Directive, OnInit } from '@angular/core';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { UserService } from 'jslib-common/abstractions/user.service';
@Directive()
export class PremiumComponent implements OnInit {
isPremium: boolean = false;
price: number = 10;
refreshPromise: Promise<any>;
constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService,
protected apiService: ApiService, protected userService: UserService) { }
async ngOnInit() {
this.isPremium = await this.userService.canAccessPremium();
}
async refresh() {
try {
this.refreshPromise = this.apiService.refreshIdentityToken();
await this.refreshPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('refreshComplete'));
this.isPremium = await this.userService.canAccessPremium();
} catch { }
}
async purchase() {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumPurchaseAlert'),
this.i18nService.t('premiumPurchase'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (confirmed) {
this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase');
}
}
async manage() {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumManageAlert'),
this.i18nService.t('premiumManage'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (confirmed) {
this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=manage');
}
}
}

View File

@@ -0,0 +1,173 @@
import { Router } from '@angular/router';
import { KeysRequest } from 'jslib-common/models/request/keysRequest';
import { ReferenceEventRequest } from 'jslib-common/models/request/referenceEventRequest';
import { RegisterRequest } from 'jslib-common/models/request/registerRequest';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { KdfType } from 'jslib-common/enums/kdfType';
export class RegisterComponent {
name: string = '';
email: string = '';
masterPassword: string = '';
confirmMasterPassword: string = '';
hint: string = '';
showPassword: boolean = false;
formPromise: Promise<any>;
masterPasswordScore: number;
referenceData: ReferenceEventRequest;
showTerms = true;
acceptPolicies: boolean = false;
protected successRoute = 'login';
private masterPasswordStrengthTimeout: any;
constructor(protected authService: AuthService, protected router: Router,
protected i18nService: I18nService, protected cryptoService: CryptoService,
protected apiService: ApiService, protected stateService: StateService,
protected platformUtilsService: PlatformUtilsService,
protected passwordGenerationService: PasswordGenerationService) {
this.showTerms = !platformUtilsService.isSelfHost();
}
get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
}
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return 'success';
case 3:
return 'primary';
case 2:
return 'warning';
default:
return 'danger';
}
}
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t('strong');
case 3:
return this.i18nService.t('good');
case 2:
return this.i18nService.t('weak');
default:
return this.masterPasswordScore != null ? this.i18nService.t('weak') : null;
}
}
async submit() {
if (!this.acceptPolicies && this.showTerms) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('acceptPoliciesError'));
return;
}
if (this.email == null || this.email === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('emailRequired'));
return;
}
if (this.email.indexOf('@') === -1) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidEmail'));
return;
}
if (this.masterPassword == null || this.masterPassword === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
if (this.masterPassword.length < 8) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassLength'));
return;
}
if (this.masterPassword !== this.confirmMasterPassword) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassDoesntMatch'));
return;
}
const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword,
this.getPasswordStrengthUserInput());
if (strengthResult != null && strengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
'warning');
if (!result) {
return;
}
}
if (this.hint === this.masterPassword) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('hintEqualsPassword'));
return;
}
this.name = this.name === '' ? null : this.name;
this.email = this.email.trim().toLowerCase();
const kdf = KdfType.PBKDF2_SHA256;
const useLowerKdf = this.platformUtilsService.isIE();
const kdfIterations = useLowerKdf ? 10000 : 100000;
const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations);
const encKey = await this.cryptoService.makeEncKey(key);
const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key);
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
const request = new RegisterRequest(this.email, this.name, hashedPassword,
this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData);
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
const orgInvite = await this.stateService.get<any>('orgInvitation');
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
request.token = orgInvite.token;
request.organizationUserId = orgInvite.organizationUserId;
}
try {
this.formPromise = this.apiService.postRegister(request);
await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('newAccountCreated'));
this.router.navigate([this.successRoute], { queryParams: { email: this.email } });
} catch { }
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus();
}
updatePasswordStrength() {
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
this.masterPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword,
this.getPasswordStrengthUserInput());
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf('@');
if (atPosition > -1) {
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
}
if (this.name != null && this.name !== '') {
userInput = userInput.concat(this.name.trim().toLowerCase().split(' '));
}
return userInput;
}
}

View File

@@ -0,0 +1,528 @@
import { DatePipe } from '@angular/common';
import {
Directive,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
import { PolicyType } from 'jslib-common/enums/policyType';
import { SendType } from 'jslib-common/enums/sendType';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { SendService } from 'jslib-common/abstractions/send.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { SendFileView } from 'jslib-common/models/view/sendFileView';
import { SendTextView } from 'jslib-common/models/view/sendTextView';
import { SendView } from 'jslib-common/models/view/sendView';
import { EncArrayBuffer } from 'jslib-common/models/domain/encArrayBuffer';
import { Send } from 'jslib-common/models/domain/send';
// TimeOption is used for the dropdown implementation of custom times
// Standard = displayed time; Military = stored time
interface TimeOption {
standard: string;
military: string;
}
enum DateField {
DeletionDate = 'deletion',
ExpriationDate = 'expiration',
}
@Directive()
export class AddEditComponent implements OnInit {
@Input() sendId: string;
@Input() type: SendType;
@Output() onSavedSend = new EventEmitter<SendView>();
@Output() onDeletedSend = new EventEmitter<SendView>();
@Output() onCancelled = new EventEmitter<SendView>();
copyLink = false;
disableSend = false;
disableHideEmail = false;
send: SendView;
deletionDate: string;
deletionDateFallback: string;
deletionTimeFallback: string;
expirationDate: string = null;
expirationDateFallback: string;
expirationTimeFallback: string;
hasPassword: boolean;
password: string;
showPassword = false;
formPromise: Promise<any>;
deletePromise: Promise<any>;
sendType = SendType;
typeOptions: any[];
deletionDateOptions: any[];
expirationDateOptions: any[];
deletionDateSelect = 168;
expirationDateSelect: number = null;
canAccessPremium = true;
emailVerified = true;
alertShown = false;
showOptions = false;
safariDeletionTime: string;
safariExpirationTime: string;
safariDeletionTimeOptions: TimeOption[];
safariExpirationTimeOptions: TimeOption[];
private sendLinkBaseUrl: string;
constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService, protected datePipe: DatePipe,
protected sendService: SendService, protected userService: UserService,
protected messagingService: MessagingService, protected policyService: PolicyService) {
this.typeOptions = [
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
];
this.deletionDateOptions = this.expirationDateOptions = [
{ name: i18nService.t('oneHour'), value: 1 },
{ name: i18nService.t('oneDay'), value: 24 },
{ name: i18nService.t('days', '2'), value: 48 },
{ name: i18nService.t('days', '3'), value: 72 },
{ name: i18nService.t('days', '7'), value: 168 },
{ name: i18nService.t('days', '30'), value: 720 },
{ name: i18nService.t('custom'), value: 0 },
];
this.expirationDateOptions = [
{ name: i18nService.t('never'), value: null },
].concat([...this.deletionDateOptions]);
const webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
this.sendLinkBaseUrl = 'https://send.bitwarden.com/#';
} else {
this.sendLinkBaseUrl = webVaultUrl + '/#/send/';
}
}
get link(): string {
if (this.send.id != null && this.send.accessId != null) {
return this.sendLinkBaseUrl + this.send.accessId + '/' + this.send.urlB64Key;
}
return null;
}
get isSafari() {
return this.platformUtilsService.isSafari();
}
get isDateTimeLocalSupported(): boolean {
return !(this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari());
}
async ngOnInit() {
await this.load();
}
get editMode(): boolean {
return this.sendId != null;
}
get title(): string {
return this.i18nService.t(
this.editMode ?
'editSend' :
'createSend'
);
}
get expirationDateTimeFallback() {
return this.nullOrWhiteSpaceCount([this.expirationDateFallback, this.expirationTimeFallback]) > 0 ?
null :
`${this.formatDateFallbacks(this.expirationDateFallback)}T${this.expirationTimeFallback}`;
}
get deletionDateTimeFallback() {
return this.nullOrWhiteSpaceCount([this.deletionDateFallback, this.deletionTimeFallback]) > 0 ?
null :
`${this.formatDateFallbacks(this.deletionDateFallback)}T${this.deletionTimeFallback}`;
}
async load() {
const disableSendPolicies = await this.policyService.getAll(PolicyType.DisableSend);
const organizations = await this.userService.getAllOrganizations();
this.disableSend = organizations.some(o => {
return o.enabled &&
o.status === OrganizationUserStatusType.Confirmed &&
o.usePolicies &&
!o.canManagePolicies &&
disableSendPolicies.some(p => p.organizationId === o.id && p.enabled);
});
const sendOptionsPolicies = await this.policyService.getAll(PolicyType.SendOptions);
this.disableHideEmail = await organizations.some(o => {
return o.enabled &&
o.status === OrganizationUserStatusType.Confirmed &&
o.usePolicies &&
!o.canManagePolicies &&
sendOptionsPolicies.some(p => p.organizationId === o.id && p.enabled && p.data.disableHideEmail);
});
this.canAccessPremium = await this.userService.canAccessPremium();
this.emailVerified = await this.userService.getEmailVerified();
if (!this.canAccessPremium || !this.emailVerified) {
this.type = SendType.Text;
}
if (this.send == null) {
if (this.editMode) {
const send = await this.loadSend();
this.send = await send.decrypt();
} else {
this.send = new SendView();
this.send.type = this.type == null ? SendType.File : this.type;
this.send.file = new SendFileView();
this.send.text = new SendTextView();
this.send.deletionDate = new Date();
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
}
}
this.hasPassword = this.send.password != null && this.send.password.trim() !== '';
// Parse dates
if (!this.isDateTimeLocalSupported) {
const deletionDateParts = this.dateToSplitString(this.send.deletionDate);
if (deletionDateParts !== undefined && deletionDateParts.length > 0) {
this.deletionDateFallback = deletionDateParts[0];
this.deletionTimeFallback = deletionDateParts[1];
if (this.isSafari) {
this.safariDeletionTime = this.deletionTimeFallback;
}
}
const expirationDateParts = this.dateToSplitString(this.send.expirationDate);
if (expirationDateParts !== undefined && expirationDateParts.length > 0) {
this.expirationDateFallback = expirationDateParts[0];
this.expirationTimeFallback = expirationDateParts[1];
if (this.isSafari) {
this.safariExpirationTime = this.expirationTimeFallback;
}
}
} else {
this.deletionDate = this.dateToString(this.send.deletionDate);
this.expirationDate = this.dateToString(this.send.expirationDate);
}
if (this.isSafari) {
this.safariDeletionTimeOptions = this.safariTimeOptions(DateField.DeletionDate);
this.safariExpirationTimeOptions = this.safariTimeOptions(DateField.ExpriationDate);
}
}
async submit(): Promise<boolean> {
if (!this.isDateTimeLocalSupported) {
if (this.isSafari) {
this.expirationTimeFallback = this.safariExpirationTime ?? this.expirationTimeFallback;
this.deletionTimeFallback = this.safariDeletionTime ?? this.deletionTimeFallback;
}
this.deletionDate = this.deletionDateTimeFallback;
if (this.expirationDateTimeFallback != null && isNaN(Date.parse(this.expirationDateTimeFallback))) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('expirationDateIsInvalid'));
return;
}
if (isNaN(Date.parse(this.deletionDateTimeFallback))) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('deletionDateIsInvalid'));
return;
}
if (this.nullOrWhiteSpaceCount([this.expirationDateFallback, this.expirationTimeFallback]) === 1) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('expirationDateAndTimeRequired'));
return;
}
if (this.editMode || this.expirationDateSelect === 0) {
this.expirationDate = this.expirationDateTimeFallback;
}
}
if (this.disableSend) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('sendDisabledWarning'));
return false;
}
if (this.send.name == null || this.send.name === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('nameRequired'));
return false;
}
let file: File = null;
if (this.send.type === SendType.File && !this.editMode) {
const fileEl = document.getElementById('file') as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectFile'));
return;
}
file = files[0];
if (files[0].size > 524288000) { // 500 MB
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('maxFileSize'));
return;
}
}
if (!this.editMode) {
const now = new Date();
if (this.deletionDateSelect > 0) {
const d = new Date();
d.setHours(now.getHours() + this.deletionDateSelect);
this.deletionDate = this.dateToString(d);
}
if (this.expirationDateSelect != null && this.expirationDateSelect > 0) {
const d = new Date();
d.setHours(now.getHours() + this.expirationDateSelect);
this.expirationDate = this.dateToString(d);
}
}
if (this.password != null && this.password.trim() === '') {
this.password = null;
}
this.formPromise = this.encryptSend(file)
.then(async encSend => {
const uploadPromise = this.sendService.saveWithServer(encSend);
await uploadPromise;
if (this.send.id == null) {
this.send.id = encSend[0].id;
}
if (this.send.accessId == null) {
this.send.accessId = encSend[0].accessId;
}
this.onSavedSend.emit(this.send);
if (this.copyLink && this.link != null) {
const copySuccess = await this.copyLinkToClipboard(this.link);
if (copySuccess ?? true) {
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
} else {
await this.platformUtilsService.showDialog(
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'), null,
this.i18nService.t('ok'), null, 'success', null);
await this.copyLinkToClipboard(this.link);
}
}
});
try {
await this.formPromise;
return true;
} catch { }
return false;
}
clearExpiration() {
this.expirationDate = null;
this.expirationDateFallback = null;
this.expirationTimeFallback = null;
this.safariExpirationTime = null;
}
async copyLinkToClipboard(link: string): Promise<void | boolean> {
return Promise.resolve(this.platformUtilsService.copyToClipboard(link));
}
async delete(): Promise<boolean> {
if (this.deletePromise != null) {
return false;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteSendConfirmation'),
this.i18nService.t('deleteSend'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.sendService.deleteWithServer(this.send.id);
await this.deletePromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.load();
this.onDeletedSend.emit(this.send);
return true;
} catch { }
return false;
}
typeChanged() {
if (this.send.type === SendType.File && !this.alertShown)
{
if (!this.canAccessPremium) {
this.alertShown = true;
this.messagingService.send('premiumRequired');
} else if (!this.emailVerified) {
this.alertShown = true;
this.messagingService.send('emailVerificationRequired');
}
}
}
toggleOptions() {
this.showOptions = !this.showOptions;
}
expirationDateFallbackChanged() {
this.isSafari ?
this.safariExpirationTime = this.safariExpirationTime ?? '00:00' :
this.expirationTimeFallback = this.expirationTimeFallback ?? this.datePipe.transform(new Date(), 'HH:mm');
}
protected async loadSend(): Promise<Send> {
return this.sendService.get(this.sendId);
}
protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> {
const sendData = await this.sendService.encrypt(this.send, file, this.password, null);
// Parse dates
try {
sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
} catch {
sendData[0].deletionDate = null;
}
try {
sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
} catch {
sendData[0].expirationDate = null;
}
return sendData;
}
protected dateToString(d: Date) {
return d == null ? null : this.datePipe.transform(d, 'yyyy-MM-ddTHH:mm');
}
protected formatDateFallbacks(dateString: string) {
try {
// The Firefox date picker doesn't supply a time, safari's polyfill does.
// Unknown if Safari's native date picker will or not when it releases.
if (!this.isSafari) {
dateString += ' 00:00';
}
return this.datePipe.transform(new Date(dateString), 'yyyy-MM-dd');
} catch {
// this should never happen
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('dateParsingError'));
}
}
protected dateToSplitString(d: Date) {
if (d != null) {
const date = !this.isSafari ?
this.datePipe.transform(d, 'yyyy-MM-dd') :
this.datePipe.transform(d, 'MM/dd/yyyy');
const time = this.datePipe.transform(d, 'HH:mm');
return [date, time];
}
}
protected togglePasswordVisible() {
this.showPassword = !this.showPassword;
document.getElementById('password').focus();
}
protected nullOrWhiteSpaceCount(strarray: string[]): number {
return strarray.filter(str => str == null || str.trim() === '').length;
}
protected safariTimeOptions(field: DateField): TimeOption[] {
// init individual arrays for major sort groups
const noon: TimeOption[] = [];
const midnight: TimeOption[] = [];
const ams: TimeOption[] = [];
const pms: TimeOption[] = [];
// determine minute skip (5 min, 10 min, 15 min, etc.)
const minuteIncrementer = 15;
// loop through each hour on a 12 hour system
for (let h = 1; h <= 12; h++) {
// loop through each minute in the hour using the skip to incriment
for (let m = 0; m < 60; m += minuteIncrementer) {
// init the final strings that will be added to the lists
let hour = h.toString();
let minutes = m.toString();
// add prepending 0s to single digit hours/minutes
if (h < 10) {
hour = '0' + hour;
}
if (m < 10) {
minutes = '0' + minutes;
}
// build time strings and push to relevant sort groups
if (h === 12) {
const midnightOption: TimeOption = {
standard: `${hour}:${minutes} AM`,
military: `00:${minutes}`,
};
midnight.push(midnightOption);
const noonOption: TimeOption = {
standard: `${hour}:${minutes} PM`,
military: `${hour}:${minutes}`,
};
noon.push(noonOption);
} else {
const amOption: TimeOption = {
standard: `${hour}:${minutes} AM`,
military: `${hour}:${minutes}`,
};
ams.push(amOption);
const pmOption: TimeOption = {
standard: `${hour}:${minutes} PM`,
military: `${h + 12}:${minutes}`,
};
pms.push(pmOption);
}
}
}
// bring all the arrays together in the right order
const validTimes = [...midnight, ...ams, ...noon, ...pms];
// determine if an unsupported value already exists on the send & add that to the top of the option list
// example: if the Send was created with a different client
if (field === DateField.ExpriationDate && this.expirationDateTimeFallback != null && this.editMode) {
const previousValue: TimeOption = {
standard: this.datePipe.transform(this.expirationDateTimeFallback, 'hh:mm a'),
military: this.datePipe.transform(this.expirationDateTimeFallback, 'HH:mm'),
};
return [previousValue, { standard: null, military: null }, ...validTimes];
} else if (field === DateField.DeletionDate && this.deletionDateTimeFallback != null && this.editMode) {
const previousValue: TimeOption = {
standard: this.datePipe.transform(this.deletionDateTimeFallback, 'hh:mm a'),
military: this.datePipe.transform(this.deletionDateTimeFallback, 'HH:mm'),
};
return [previousValue, ...validTimes];
} else {
return [{ standard: null, military: null }, ...validTimes];
}
}
}

View File

@@ -0,0 +1,210 @@
import {
Directive,
NgZone,
OnInit,
} from '@angular/core';
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
import { PolicyType } from 'jslib-common/enums/policyType';
import { SendType } from 'jslib-common/enums/sendType';
import { SendView } from 'jslib-common/models/view/sendView';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { SendService } from 'jslib-common/abstractions/send.service';
import { UserService } from 'jslib-common/abstractions/user.service';
@Directive()
export class SendComponent implements OnInit {
disableSend = false;
sendType = SendType;
loaded = false;
loading = true;
refreshing = false;
expired: boolean = false;
type: SendType = null;
sends: SendView[] = [];
filteredSends: SendView[] = [];
searchText: string;
selectedType: SendType;
selectedAll: boolean;
searchPlaceholder: string;
filter: (cipher: SendView) => boolean;
searchPending = false;
hasSearched = false; // search() function called - returns true if text qualifies for search
actionPromise: any;
onSuccessfulRemovePassword: () => Promise<any>;
onSuccessfulDelete: () => Promise<any>;
onSuccessfulLoad: () => Promise<any>;
private searchTimeout: any;
constructor(protected sendService: SendService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService,
protected ngZone: NgZone, protected searchService: SearchService,
protected policyService: PolicyService, protected userService: UserService) { }
async ngOnInit() {
const policies = await this.policyService.getAll(PolicyType.DisableSend);
const organizations = await this.userService.getAllOrganizations();
this.disableSend = organizations.some(o => {
return o.enabled &&
o.status === OrganizationUserStatusType.Confirmed &&
o.usePolicies &&
!o.canManagePolicies &&
policies.some(p => p.organizationId === o.id && p.enabled);
});
}
async load(filter: (send: SendView) => boolean = null) {
this.loading = true;
const sends = await this.sendService.getAllDecrypted();
this.sends = sends;
if (this.onSuccessfulLoad != null) {
await this.onSuccessfulLoad();
} else {
// Default action
this.selectAll();
}
this.loading = false;
this.loaded = true;
}
async reload(filter: (send: SendView) => boolean = null) {
this.loaded = false;
this.sends = [];
await this.load(filter);
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter);
} finally {
this.refreshing = false;
}
}
async applyFilter(filter: (send: SendView) => boolean = null) {
this.filter = filter;
await this.search(null);
}
async search(timeout: number = null) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText);
this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s));
this.applyTextSearch();
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.searchService.isSearchable(this.searchText);
this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s));
this.applyTextSearch();
this.searchPending = false;
}, timeout);
}
async removePassword(s: SendView): Promise<boolean> {
if (this.actionPromise != null || s.password == null) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'),
this.i18nService.t('removePassword'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.sendService.removePasswordWithServer(s.id);
await this.actionPromise;
if (this.onSuccessfulRemovePassword != null) {
this.onSuccessfulRemovePassword();
} else {
// Default actions
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
await this.load();
}
} catch { }
this.actionPromise = null;
}
async delete(s: SendView): Promise<boolean> {
if (this.actionPromise != null) {
return false;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('deleteSendConfirmation'),
this.i18nService.t('deleteSend'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.sendService.deleteWithServer(s.id);
await this.actionPromise;
if (this.onSuccessfulDelete != null) {
this.onSuccessfulDelete();
} else {
// Default actions
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
await this.refresh();
}
} catch { }
this.actionPromise = null;
return true;
}
copy(s: SendView) {
let sendLinkBaseUrl = 'https://send.bitwarden.com/#';
const webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl != null) {
sendLinkBaseUrl = webVaultUrl + '/#/send/';
}
const link = sendLinkBaseUrl + s.accessId + '/' + s.urlB64Key;
this.platformUtilsService.copyToClipboard(link);
this.platformUtilsService.showToast('success', null,
this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
}
searchTextChanged() {
this.search(200);
}
selectAll() {
this.clearSelections();
this.selectedAll = true;
this.applyFilter(null);
}
selectType(type: SendType) {
this.clearSelections();
this.selectedType = type;
this.applyFilter(s => s.type === type);
}
clearSelections() {
this.selectedAll = false;
this.selectedType = null;
}
private applyTextSearch() {
if (this.searchText != null) {
this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText);
}
}
}

View File

@@ -0,0 +1,107 @@
import { Directive } from '@angular/core';
import {
ActivatedRoute,
Router
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { EncString } from 'jslib-common/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { KeysRequest } from 'jslib-common/models/request/keysRequest';
import { SetPasswordRequest } from 'jslib-common/models/request/setPasswordRequest';
import { ChangePasswordComponent as BaseChangePasswordComponent } from './change-password.component';
import { KdfType } from 'jslib-common/enums/kdfType';
@Directive()
export class SetPasswordComponent extends BaseChangePasswordComponent {
syncLoading: boolean = true;
showPassword: boolean = false;
hint: string = '';
identifier: string = null;
onSuccessfulChangePassword: () => Promise<any>;
successRoute = 'vault';
constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService,
userService: UserService, passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService, policyService: PolicyService, private router: Router,
private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute) {
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService, policyService);
}
async ngOnInit() {
await this.syncService.fullSync(true);
this.syncLoading = false;
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.identifier != null) {
this.identifier = qParams.identifier;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
super.ngOnInit();
}
async setupSubmitActions() {
this.kdf = KdfType.PBKDF2_SHA256;
const useLowerKdf = this.platformUtilsService.isIE();
this.kdfIterations = useLowerKdf ? 10000 : 100000;
return true;
}
async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey,
encKey: [SymmetricCryptoKey, EncString]) {
const request = new SetPasswordRequest();
request.masterPasswordHash = masterPasswordHash;
request.key = encKey[1].encryptedString;
request.masterPasswordHint = this.hint;
request.kdf = this.kdf;
request.kdfIterations = this.kdfIterations;
request.orgIdentifier = this.identifier;
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
try {
this.formPromise = this.apiService.setPassword(request);
await this.formPromise;
await this.userService.setInformation(await this.userService.getUserId(), await this.userService.getEmail(),
this.kdf, this.kdfIterations);
await this.cryptoService.setKey(key);
await this.cryptoService.setKeyHash(masterPasswordHash);
await this.cryptoService.setEncKey(encKey[1].encryptedString);
await this.cryptoService.setEncPrivateKey(keys[1].encryptedString);
if (this.onSuccessfulChangePassword != null) {
this.onSuccessfulChangePassword();
} else {
this.router.navigate([this.successRoute]);
}
} catch {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
}
togglePassword(confirmField: boolean) {
this.showPassword = !this.showPassword;
document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus();
}
}

View File

@@ -0,0 +1,103 @@
import {
Directive,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CollectionService } from 'jslib-common/abstractions/collection.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { Organization } from 'jslib-common/models/domain/organization';
import { CipherView } from 'jslib-common/models/view/cipherView';
import { CollectionView } from 'jslib-common/models/view/collectionView';
import { Utils } from 'jslib-common/misc/utils';
@Directive()
export class ShareComponent implements OnInit {
@Input() cipherId: string;
@Input() organizationId: string;
@Output() onSharedCipher = new EventEmitter();
formPromise: Promise<any>;
cipher: CipherView;
collections: CollectionView[] = [];
organizations: Organization[] = [];
protected writeableCollections: CollectionView[] = [];
constructor(protected collectionService: CollectionService, protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService, protected userService: UserService,
protected cipherService: CipherService) { }
async ngOnInit() {
await this.load();
}
async load() {
const allCollections = await this.collectionService.getAllDecrypted();
this.writeableCollections = allCollections.map(c => c).filter(c => !c.readOnly);
const orgs = await this.userService.getAllOrganizations();
this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name'))
.filter(o => o.enabled && o.status === OrganizationUserStatusType.Confirmed);
const cipherDomain = await this.cipherService.get(this.cipherId);
this.cipher = await cipherDomain.decrypt();
if (this.organizationId == null && this.organizations.length > 0) {
this.organizationId = this.organizations[0].id;
}
this.filterCollections();
}
filterCollections() {
this.writeableCollections.forEach(c => (c as any).checked = false);
if (this.organizationId == null || this.writeableCollections.length === 0) {
this.collections = [];
} else {
this.collections = this.writeableCollections.filter(c => c.organizationId === this.organizationId);
}
}
async submit(): Promise<boolean> {
const selectedCollectionIds = this.collections
.filter(c => !!(c as any).checked)
.map(c => c.id);
if (selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectOneCollection'));
return;
}
const cipherDomain = await this.cipherService.get(this.cipherId);
const cipherView = await cipherDomain.decrypt();
try {
this.formPromise = this.cipherService.shareWithServer(cipherView, this.organizationId,
selectedCollectionIds).then(async () => {
this.onSharedCipher.emit();
this.platformUtilsService.showToast('success', null, this.i18nService.t('sharedItem'));
});
await this.formPromise;
return true;
} catch { }
return false;
}
get canSave() {
if (this.collections != null) {
for (let i = 0; i < this.collections.length; i++) {
if ((this.collections[i] as any).checked) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,200 @@
import { Directive } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { Utils } from 'jslib-common/misc/utils';
import { AuthResult } from 'jslib-common/models/domain/authResult';
@Directive()
export class SsoComponent {
identifier: string;
loggingIn = false;
formPromise: Promise<AuthResult>;
initiateSsoFormPromise: Promise<any>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLoginChangePasswordNavigate: () => Promise<any>;
protected twoFactorRoute = '2fa';
protected successRoute = 'lock';
protected changePasswordRoute = 'set-password';
protected clientId: string;
protected redirectUri: string;
protected state: string;
protected codeChallenge: string;
constructor(protected authService: AuthService, protected router: Router,
protected i18nService: I18nService, protected route: ActivatedRoute,
protected storageService: StorageService, protected stateService: StateService,
protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService,
protected cryptoFunctionService: CryptoFunctionService,
protected passwordGenerationService: PasswordGenerationService) { }
async ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.code != null && qParams.state != null) {
const codeVerifier = await this.storageService.get<string>(ConstantsService.ssoCodeVerifierKey);
const state = await this.storageService.get<string>(ConstantsService.ssoStateKey);
await this.storageService.remove(ConstantsService.ssoCodeVerifierKey);
await this.storageService.remove(ConstantsService.ssoStateKey);
if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) {
await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(qParams.state));
}
} else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null &&
qParams.codeChallenge != null) {
this.redirectUri = qParams.redirectUri;
this.state = qParams.state;
this.codeChallenge = qParams.codeChallenge;
this.clientId = qParams.clientId;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
}
async submit(returnUri?: string, includeUserIdentifier?: boolean) {
this.initiateSsoFormPromise = this.preValidate();
if (await this.initiateSsoFormPromise) {
const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier);
this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true });
}
}
async preValidate(): Promise<boolean> {
if (this.identifier == null || this.identifier === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('ssoValidationFailed'),
this.i18nService.t('ssoIdentifierRequired'));
return false;
}
return await this.apiService.preValidateSso(this.identifier);
}
protected async buildAuthorizeUrl(returnUri?: string, includeUserIdentifier?: boolean): Promise<string> {
let codeChallenge = this.codeChallenge;
let state = this.state;
const passwordOptions: any = {
type: 'password',
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
if (codeChallenge == null) {
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256');
codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier);
}
if (state == null) {
state = await this.passwordGenerationService.generatePassword(passwordOptions);
if (returnUri) {
state += `_returnUri='${returnUri}'`;
}
}
// Add Organization Identifier to state
state += `_identifier=${this.identifier}`;
// Save state (regardless of new or existing)
await this.storageService.save(ConstantsService.ssoStateKey, state);
let authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' +
'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' +
'response_type=code&scope=api offline_access&' +
'state=' + state + '&code_challenge=' + codeChallenge + '&' +
'code_challenge_method=S256&response_mode=query&' +
'domain_hint=' + encodeURIComponent(this.identifier);
if (includeUserIdentifier) {
const userIdentifier = await this.apiService.getSsoUserIdentifier();
authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`;
}
return authorizeUrl;
}
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
this.loggingIn = true;
try {
this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri);
const response = await this.formPromise;
if (response.twoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
this.onSuccessfulLoginTwoFactorNavigate();
} else {
this.router.navigate([this.twoFactorRoute], {
queryParams: {
identifier: orgIdFromState,
sso: 'true',
},
});
}
} else if (response.resetMasterPassword) {
if (this.onSuccessfulLoginChangePasswordNavigate != null) {
this.onSuccessfulLoginChangePasswordNavigate();
} else {
this.router.navigate([this.changePasswordRoute], {
queryParams: {
identifier: orgIdFromState,
},
});
}
} else {
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute]);
}
}
} catch { }
this.loggingIn = false;
}
private getOrgIdentiferFromState(state: string): string {
if (state === null || state === undefined) {
return null;
}
const stateSplit = state.split('_identifier=');
return stateSplit.length > 1 ? stateSplit[1] : null;
}
private checkState(state: string, checkState: string): boolean {
if (state === null || state === undefined) {
return false;
}
if (checkState === null || checkState === undefined) {
return false;
}
const stateSplit = state.split('_identifier=');
const checkStateSplit = checkState.split('_identifier=');
return stateSplit[0] === checkStateSplit[0];
}
}

View File

@@ -0,0 +1,38 @@
import {
Directive,
EventEmitter,
OnInit,
Output,
} from '@angular/core';
import { Router } from '@angular/router';
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
@Directive()
export class TwoFactorOptionsComponent implements OnInit {
@Output() onProviderSelected = new EventEmitter<TwoFactorProviderType>();
@Output() onRecoverSelected = new EventEmitter();
providers: any[] = [];
constructor(protected authService: AuthService, protected router: Router,
protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService,
protected win: Window) { }
ngOnInit() {
this.providers = this.authService.getSupportedTwoFactorProviders(this.win);
}
choose(p: any) {
this.onProviderSelected.emit(p.type);
}
recover() {
this.platformUtilsService.launchUri('https://help.bitwarden.com/article/lost-two-step-device/');
this.onRecoverSelected.emit();
}
}

View File

@@ -0,0 +1,238 @@
import { Directive, OnDestroy, OnInit } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType';
import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest';
import { AuthResult } from 'jslib-common/models/domain';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { TwoFactorProviders } from 'jslib-common/services/auth.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import * as DuoWebSDK from 'duo_web_sdk';
import { WebAuthn } from 'jslib-common/misc/webauthn';
@Directive()
export class TwoFactorComponent implements OnInit, OnDestroy {
token: string = '';
remember: boolean = false;
webAuthnReady: boolean = false;
webAuthnNewTab: boolean = false;
providers = TwoFactorProviders;
providerType = TwoFactorProviderType;
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
webAuthnSupported: boolean = false;
webAuthn: WebAuthn = null;
title: string = '';
twoFactorEmail: string = null;
formPromise: Promise<any>;
emailPromise: Promise<any>;
identifier: string = null;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
protected loginRoute = 'login';
protected successRoute = 'vault';
constructor(protected authService: AuthService, protected router: Router,
protected i18nService: I18nService, protected apiService: ApiService,
protected platformUtilsService: PlatformUtilsService, protected win: Window,
protected environmentService: EnvironmentService, protected stateService: StateService,
protected storageService: StorageService, protected route: ActivatedRoute) {
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
}
async ngOnInit() {
if (!this.authing || this.authService.twoFactorProvidersData == null) {
this.router.navigate([this.loginRoute]);
return;
}
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (qParams.identifier != null) {
this.identifier = qParams.identifier;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
if (this.needsLock) {
this.successRoute = 'lock';
}
if (this.win != null && this.webAuthnSupported) {
let webVaultUrl = this.environmentService.getWebVaultUrl();
if (webVaultUrl == null) {
webVaultUrl = 'https://vault.bitwarden.com';
}
this.webAuthn = new WebAuthn(this.win, webVaultUrl, this.webAuthnNewTab, this.platformUtilsService,
this.i18nService, (token: string) => {
this.token = token;
this.submit();
}, (error: string) => {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error);
}, (info: string) => {
if (info === 'ready') {
this.webAuthnReady = true;
}
}
);
}
this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.webAuthnSupported);
await this.init();
}
ngOnDestroy(): void {
this.cleanupWebAuthn();
this.webAuthn = null;
}
async init() {
if (this.selectedProviderType == null) {
this.title = this.i18nService.t('loginUnavailable');
return;
}
this.cleanupWebAuthn();
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
switch (this.selectedProviderType) {
case TwoFactorProviderType.WebAuthn:
if (!this.webAuthnSupported || this.webAuthn == null) {
break;
}
setTimeout(() => {
this.webAuthn.init(providerData);
}, 500);
break;
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
setTimeout(() => {
DuoWebSDK.init({
iframe: undefined,
host: providerData.Host,
sig_request: providerData.Signature,
submit_callback: async (f: HTMLFormElement) => {
const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement;
if (sig != null) {
this.token = sig.value;
await this.submit();
}
},
});
}, 0);
break;
case TwoFactorProviderType.Email:
this.twoFactorEmail = providerData.Email;
if (this.authService.twoFactorProvidersData.size > 1) {
await this.sendEmail(false);
}
break;
default:
break;
}
}
async submit() {
if (this.token == null || this.token === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('verificationCodeRequired'));
return;
}
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn) {
if (this.webAuthn != null) {
this.webAuthn.stop();
} else {
return;
}
} else if (this.selectedProviderType === TwoFactorProviderType.Email ||
this.selectedProviderType === TwoFactorProviderType.Authenticator) {
this.token = this.token.replace(' ', '').trim();
}
try {
await this.doSubmit();
} catch {
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) {
this.webAuthn.start();
}
}
}
async doSubmit() {
this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember);
const response: AuthResult = await this.formPromise;
const disableFavicon = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (response.resetMasterPassword) {
this.successRoute = 'set-password';
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute], {
queryParams: {
identifier: this.identifier,
},
});
}
}
async sendEmail(doToast: boolean) {
if (this.selectedProviderType !== TwoFactorProviderType.Email) {
return;
}
if (this.emailPromise != null) {
return;
}
try {
const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash);
this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise;
if (doToast) {
this.platformUtilsService.showToast('success', null,
this.i18nService.t('verificationCodeEmailSent', this.twoFactorEmail));
}
} catch { }
this.emailPromise = null;
}
private cleanupWebAuthn() {
if (this.webAuthn != null) {
this.webAuthn.stop();
this.webAuthn.cleanup();
}
}
get authing(): boolean {
return this.authService.authingWithPassword() || this.authService.authingWithSso() || this.authService.authingWithApiKey();
}
get needsLock(): boolean {
return this.authService.authingWithSso() || this.authService.authingWithApiKey();
}
}

View File

@@ -0,0 +1,402 @@
import {
ChangeDetectorRef,
Directive,
EventEmitter,
Input,
NgZone,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType';
import { CipherType } from 'jslib-common/enums/cipherType';
import { EventType } from 'jslib-common/enums/eventType';
import { FieldType } from 'jslib-common/enums/fieldType';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuditService } from 'jslib-common/abstractions/audit.service';
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EventService } from 'jslib-common/abstractions/event.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { TotpService } from 'jslib-common/abstractions/totp.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ErrorResponse } from 'jslib-common/models/response/errorResponse';
import { AttachmentView } from 'jslib-common/models/view/attachmentView';
import { CipherView } from 'jslib-common/models/view/cipherView';
import { FieldView } from 'jslib-common/models/view/fieldView';
import { LoginUriView } from 'jslib-common/models/view/loginUriView';
const BroadcasterSubscriptionId = 'ViewComponent';
@Directive()
export class ViewComponent implements OnDestroy, OnInit {
@Input() cipherId: string;
@Output() onEditCipher = new EventEmitter<CipherView>();
@Output() onCloneCipher = new EventEmitter<CipherView>();
@Output() onShareCipher = new EventEmitter<CipherView>();
@Output() onDeletedCipher = new EventEmitter<CipherView>();
@Output() onRestoredCipher = new EventEmitter<CipherView>();
cipher: CipherView;
showPassword: boolean;
showCardNumber: boolean;
showCardCode: boolean;
canAccessPremium: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
totpSec: number;
totpLow: boolean;
fieldType = FieldType;
checkPasswordPromise: Promise<number>;
private totpInterval: any;
private previousCipherId: string;
private passwordReprompted: boolean = false;
constructor(protected cipherService: CipherService, protected totpService: TotpService,
protected tokenService: TokenService, protected i18nService: I18nService,
protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService,
protected auditService: AuditService, protected win: Window,
protected broadcasterService: BroadcasterService, protected ngZone: NgZone,
protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService,
protected eventService: EventService, protected apiService: ApiService,
protected passwordRepromptService: PasswordRepromptService) { }
ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncCompleted':
if (message.successfully) {
await this.load();
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.cleanUp();
}
async load() {
this.cleanUp();
const cipher = await this.cipherService.get(this.cipherId);
this.cipher = await cipher.decrypt();
this.canAccessPremium = await this.userService.canAccessPremium();
if (this.cipher.type === CipherType.Login && this.cipher.login.totp &&
(cipher.organizationUseTotp || this.canAccessPremium)) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
this.totpInterval = setInterval(async () => {
await this.totpTick(interval);
}, 1000);
}
if (this.previousCipherId !== this.cipherId) {
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
}
this.previousCipherId = this.cipherId;
}
async edit() {
if (await this.promptPassword()) {
this.onEditCipher.emit(this.cipher);
return true;
}
return false;
}
async clone() {
if (await this.promptPassword()) {
this.onCloneCipher.emit(this.cipher);
return true;
}
return false;
}
async share() {
if (await this.promptPassword()) {
this.onShareCipher.emit(this.cipher);
return true;
}
return false;
}
async delete(): Promise<boolean> {
if (!await this.promptPassword()) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'),
this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
await this.deleteCipher();
this.platformUtilsService.showToast('success', null,
this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem'));
this.onDeletedCipher.emit(this.cipher);
} catch { }
return true;
}
async restore(): Promise<boolean> {
if (!this.cipher.isDeleted) {
return false;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
await this.restoreCipher();
this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem'));
this.onRestoredCipher.emit(this.cipher);
} catch { }
return true;
}
async togglePassword() {
if (!await this.promptPassword()) {
return;
}
this.showPassword = !this.showPassword;
if (this.showPassword) {
this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId);
}
}
async toggleCardNumber() {
if (!await this.promptPassword()) {
return;
}
this.showCardNumber = !this.showCardNumber;
if (this.showCardNumber) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
}
}
async toggleCardCode() {
if (!await this.promptPassword()) {
return;
}
this.showCardCode = !this.showCardCode;
if (this.showCardCode) {
this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId);
}
}
async checkPassword() {
if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') {
return;
}
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
const matches = await this.checkPasswordPromise;
if (matches > 0) {
this.platformUtilsService.showToast('warning', null,
this.i18nService.t('passwordExposed', matches.toString()));
} else {
this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordSafe'));
}
}
async toggleFieldValue(field: FieldView) {
if (!await this.promptPassword()) {
return;
}
const f = (field as any);
f.showValue = !f.showValue;
if (f.showValue) {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
}
}
launch(uri: LoginUriView, cipherId?: string) {
if (!uri.canLaunch) {
return;
}
if (cipherId) {
this.cipherService.updateLastLaunchedDate(cipherId);
}
this.platformUtilsService.launchUri(uri.launchUri);
}
async copy(value: string, typeI18nKey: string, aType: string) {
if (value == null) {
return;
}
if (this.passwordRepromptService.protectedFields().includes(aType) && !await this.promptPassword()) {
return;
}
const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(value, copyOptions);
this.platformUtilsService.showToast('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
if (typeI18nKey === 'password') {
this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId);
} else if (typeI18nKey === 'securityCode') {
this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
} else if (aType === 'H_Field') {
this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
}
}
setTextDataOnDrag(event: DragEvent, data: string) {
event.dataTransfer.setData('text', data);
}
async downloadAttachment(attachment: AttachmentView) {
if (!await this.promptPassword()) {
return;
}
const a = (attachment as any);
if (a.downloading) {
return;
}
if (this.cipher.organizationId == null && !this.canAccessPremium) {
this.platformUtilsService.showToast('error', this.i18nService.t('premiumRequired'),
this.i18nService.t('premiumRequiredDesc'));
return;
}
let url: string;
try {
const attachmentDownloadResponse = await this.apiService.getAttachmentData(this.cipher.id, attachment.id);
url = attachmentDownloadResponse.url;
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = attachment.url;
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
a.downloading = true;
const response = await fetch(new Request(url, { cache: 'no-store' }));
if (response.status !== 200) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
a.downloading = false;
return;
}
try {
const buf = await response.arrayBuffer();
const key = attachment.key != null ? attachment.key :
await this.cryptoService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName);
} catch (e) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
}
a.downloading = false;
}
protected deleteCipher() {
return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id)
: this.cipherService.softDeleteWithServer(this.cipher.id);
}
protected restoreCipher() {
return this.cipherService.restoreWithServer(this.cipher.id);
}
protected async promptPassword() {
if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) {
return true;
}
return this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt();
}
private cleanUp() {
this.totpCode = null;
this.cipher = null;
this.showPassword = false;
this.showCardNumber = false;
this.showCardCode = false;
this.passwordReprompted = false;
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
}
private async totpUpdateCode() {
if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) {
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
return;
}
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
if (this.totpCode != null) {
if (this.totpCode.length > 4) {
const half = Math.floor(this.totpCode.length / 2);
this.totpCodeFormatted = this.totpCode.substring(0, half) + ' ' + this.totpCode.substring(half);
} else {
this.totpCodeFormatted = this.totpCode;
}
} else {
this.totpCodeFormatted = null;
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
}
}
private async totpTick(intervalSeconds: number) {
const epoch = Math.round(new Date().getTime() / 1000.0);
const mod = epoch % intervalSeconds;
this.totpSec = intervalSeconds - mod;
this.totpDash = +(Math.round((((78.6 / intervalSeconds) * mod) + 'e+2') as any) + 'e-2');
this.totpLow = this.totpSec <= 7;
if (mod === 0) {
await this.totpUpdateCode();
}
}
}