diff --git a/angular/src/components/set-password.component.ts b/angular/src/components/set-password.component.ts index 4bcd4675cf..784b063a93 100644 --- a/angular/src/components/set-password.component.ts +++ b/angular/src/components/set-password.component.ts @@ -37,7 +37,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, userService: UserService, passwordGenerationService: PasswordGenerationService, - platformUtilsService: PlatformUtilsService, policyService: PolicyService, private router: Router, + platformUtilsService: PlatformUtilsService, policyService: PolicyService, protected router: Router, private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute) { super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, platformUtilsService, policyService); diff --git a/angular/src/components/update-temp-password.component.ts b/angular/src/components/update-temp-password.component.ts new file mode 100644 index 0000000000..c0e6b46d22 --- /dev/null +++ b/angular/src/components/update-temp-password.component.ts @@ -0,0 +1,97 @@ +import { Directive } from '@angular/core'; + +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 { UserService } from 'jslib-common/abstractions/user.service'; + +import { ChangePasswordComponent as BaseChangePasswordComponent } from './change-password.component'; + +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 { UpdateTempPasswordRequest } from 'jslib-common/models/request/updateTempPasswordRequest'; + +@Directive() +export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { + hint: string; + key: string; + enforcedPolicyOptions: MasterPasswordPolicyOptions; + showPassword: boolean = false; + + onSuccessfulChangePassword: () => Promise; + + constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, + passwordGenerationService: PasswordGenerationService, policyService: PolicyService, + cryptoService: CryptoService, userService: UserService, + messagingService: MessagingService, private apiService: ApiService) { + super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, + platformUtilsService, policyService); + } + + togglePassword(confirmField: boolean) { + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); + } + + async setupSubmitActions(): Promise { + this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + this.email = await this.userService.getEmail(); + this.kdf = await this.userService.getKdf(); + this.kdfIterations = await this.userService.getKdfIterations(); + return true; + } + + async submit() { + // Validation + if (!await this.strongPassword()) { + return; + } + + if (!await this.setupSubmitActions()) { + return; + } + + try { + // Create new key and hash new password + const newKey = await this.cryptoService.makeKey(this.masterPassword, this.email.trim().toLowerCase(), + this.kdf, this.kdfIterations); + const newPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey); + + // Grab user's current enc key + const userEncKey = await this.cryptoService.getEncKey(); + + // Create new encKey for the User + const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey); + + await this.performSubmitActions(newPasswordHash, newKey, newEncKey); + } catch { } + } + + async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, EncString]) { + try { + // Create request + const request = new UpdateTempPasswordRequest(); + request.key = encKey[1].encryptedString; + request.newMasterPasswordHash = masterPasswordHash; + request.masterPasswordHint = this.hint; + + // Update user's password + this.formPromise = this.apiService.putUpdateTempPassword(request); + await this.formPromise; + this.platformUtilsService.showToast('success', null, this.i18nService.t('updatedMasterPassword')); + + if (this.onSuccessfulChangePassword != null) { + this.onSuccessfulChangePassword(); + } else { + this.messagingService.send('logout'); + } + } catch { } + } +} diff --git a/common/src/abstractions/api.service.ts b/common/src/abstractions/api.service.ts index b4541856a0..893aaf3ddd 100644 --- a/common/src/abstractions/api.service.ts +++ b/common/src/abstractions/api.service.ts @@ -76,6 +76,7 @@ import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryReq import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; import { UpdateKeyRequest } from '../models/request/updateKeyRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; +import { UpdateTempPasswordRequest } from '../models/request/updateTempPasswordRequest'; import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; @@ -191,6 +192,7 @@ export abstract class ApiService { getEnterprisePortalSignInToken: () => Promise; postUserApiKey: (id: string, request: PasswordVerificationRequest) => Promise; postUserRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; + putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise; getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; diff --git a/common/src/abstractions/user.service.ts b/common/src/abstractions/user.service.ts index 8480c10081..1cdf453dd4 100644 --- a/common/src/abstractions/user.service.ts +++ b/common/src/abstractions/user.service.ts @@ -9,12 +9,14 @@ export abstract class UserService { setInformation: (userId: string, email: string, kdf: KdfType, kdfIterations: number) => Promise; setEmailVerified: (emailVerified: boolean) => Promise; setSecurityStamp: (stamp: string) => Promise; + setForcePasswordReset: (forcePasswordReset: boolean) => Promise; getUserId: () => Promise; getEmail: () => Promise; getSecurityStamp: () => Promise; getKdf: () => Promise; getKdfIterations: () => Promise; getEmailVerified: () => Promise; + getForcePasswordReset: () => Promise; clear: () => Promise; isAuthenticated: () => Promise; canAccessPremium: () => Promise; diff --git a/common/src/enums/eventType.ts b/common/src/enums/eventType.ts index 46315226c0..54f5ef8449 100644 --- a/common/src/enums/eventType.ts +++ b/common/src/enums/eventType.ts @@ -7,6 +7,7 @@ export enum EventType { User_FailedLogIn = 1005, User_FailedLogIn2fa = 1006, User_ClientExportedVault = 1007, + User_UpdatedTempPassword = 1008, Cipher_Created = 1100, Cipher_Updated = 1101, diff --git a/common/src/models/request/updateTempPasswordRequest.ts b/common/src/models/request/updateTempPasswordRequest.ts new file mode 100644 index 0000000000..044311d3e2 --- /dev/null +++ b/common/src/models/request/updateTempPasswordRequest.ts @@ -0,0 +1,5 @@ +import { OrganizationUserResetPasswordRequest } from './organizationUserResetPasswordRequest'; + +export class UpdateTempPasswordRequest extends OrganizationUserResetPasswordRequest { + masterPasswordHint: string; +} diff --git a/common/src/models/response/profileResponse.ts b/common/src/models/response/profileResponse.ts index fadda34235..3185cffbef 100644 --- a/common/src/models/response/profileResponse.ts +++ b/common/src/models/response/profileResponse.ts @@ -15,6 +15,7 @@ export class ProfileResponse extends BaseResponse { key: string; privateKey: string; securityStamp: string; + forcePasswordReset: boolean; organizations: ProfileOrganizationResponse[] = []; providers: ProfileProviderResponse[] = []; providerOrganizations: ProfileProviderOrganizationResponse[] = []; @@ -32,6 +33,7 @@ export class ProfileResponse extends BaseResponse { this.key = this.getResponseProperty('Key'); this.privateKey = this.getResponseProperty('PrivateKey'); this.securityStamp = this.getResponseProperty('SecurityStamp'); + this.forcePasswordReset = this.getResponseProperty('ForcePasswordReset'); const organizations = this.getResponseProperty('Organizations'); if (organizations != null) { diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index d86fefd974..f158f1c8ff 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -78,6 +78,7 @@ import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryReq import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; import { UpdateKeyRequest } from '../models/request/updateKeyRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; +import { UpdateTempPasswordRequest } from '../models/request/updateTempPasswordRequest'; import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; @@ -389,6 +390,10 @@ export class ApiService implements ApiServiceAbstraction { return new ApiKeyResponse(r); } + putUpdateTempPassword(request: UpdateTempPasswordRequest): Promise { + return this.send('PUT', '/accounts/update-temp-password', request, true, false); + } + // Folder APIs async getFolder(id: string): Promise { diff --git a/common/src/services/sync.service.ts b/common/src/services/sync.service.ts index 5df582ecae..9484929c01 100644 --- a/common/src/services/sync.service.ts +++ b/common/src/services/sync.service.ts @@ -292,6 +292,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.userService.setSecurityStamp(response.securityStamp); await this.userService.setEmailVerified(response.emailVerified); + await this.userService.setForcePasswordReset(response.forcePasswordReset); const organizations: { [id: string]: OrganizationData; } = {}; response.organizations.forEach(o => { diff --git a/common/src/services/user.service.ts b/common/src/services/user.service.ts index 15481a6646..df99b369c8 100644 --- a/common/src/services/user.service.ts +++ b/common/src/services/user.service.ts @@ -18,6 +18,7 @@ const Keys = { organizationsPrefix: 'organizations_', providersPrefix: 'providers_', emailVerified: 'emailVerified', + forcePasswordReset: 'forcePasswordReset', }; export class UserService implements UserServiceAbstraction { @@ -27,6 +28,7 @@ export class UserService implements UserServiceAbstraction { private kdf: KdfType; private kdfIterations: number; private emailVerified: boolean; + private forcePasswordReset: boolean; constructor(private tokenService: TokenService, private storageService: StorageService) { } @@ -52,6 +54,11 @@ export class UserService implements UserServiceAbstraction { return this.storageService.save(Keys.emailVerified, emailVerified); } + setForcePasswordReset(forcePasswordReset: boolean) { + this.forcePasswordReset = forcePasswordReset; + return this.storageService.save(Keys.forcePasswordReset, forcePasswordReset); + } + async getUserId(): Promise { if (this.userId == null) { this.userId = await this.storageService.get(Keys.userId); @@ -94,6 +101,13 @@ export class UserService implements UserServiceAbstraction { return this.emailVerified; } + async getForcePasswordReset(): Promise { + if (this.forcePasswordReset == null) { + this.forcePasswordReset = await this.storageService.get(Keys.forcePasswordReset); + } + return this.forcePasswordReset; + } + async clear(): Promise { const userId = await this.getUserId(); @@ -102,6 +116,7 @@ export class UserService implements UserServiceAbstraction { await this.storageService.remove(Keys.stamp); await this.storageService.remove(Keys.kdf); await this.storageService.remove(Keys.kdfIterations); + await this.storageService.remove(Keys.forcePasswordReset); await this.clearOrganizations(userId); await this.clearProviders(userId);