diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index debb78580d..9b9bf73c0a 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -27,6 +27,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; +import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { StorageRequest } from '../models/request/storageRequest'; @@ -70,6 +71,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; +import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -93,6 +95,7 @@ export abstract class ApiService { getProfile: () => Promise; getUserBilling: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; + postPrelogin: (request: PreloginRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 7b721433c0..b654365b15 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,6 +1,7 @@ import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; import { AuthResult } from '../models/domain/authResult'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class AuthService { email: string; @@ -16,4 +17,5 @@ export abstract class AuthService { logOut: (callback: Function) => void; getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; + makePreloginKey: (masterPassword: string, email: string) => Promise; } diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 9014a77726..8181ccf587 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -3,6 +3,8 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; +import { KdfType } from '../enums/kdfType'; + export abstract class CryptoService { setKey: (key: SymmetricCryptoKey) => Promise; setKeyHash: (keyHash: string) => Promise<{}>; @@ -25,7 +27,7 @@ export abstract class CryptoService { clearOrgKeys: (memoryOnly?: boolean) => Promise; clearKeys: () => Promise; toggleKey: () => Promise; - makeKey: (password: string, salt: string) => Promise; + makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; diff --git a/src/abstractions/user.service.ts b/src/abstractions/user.service.ts index 1a16c4a1fa..ab2239a845 100644 --- a/src/abstractions/user.service.ts +++ b/src/abstractions/user.service.ts @@ -1,16 +1,16 @@ import { OrganizationData } from '../models/data/organizationData'; import { Organization } from '../models/domain/organization'; -export abstract class UserService { - userId: string; - email: string; - stamp: string; +import { KdfType } from '../enums/kdfType'; - setUserIdAndEmail: (userId: string, email: string) => Promise; +export abstract class UserService { + setInformation: (userId: string, email: string, kdf: KdfType, kdfIterations: number) => Promise; setSecurityStamp: (stamp: string) => Promise; getUserId: () => Promise; getEmail: () => Promise; getSecurityStamp: () => Promise; + getKdf: () => Promise; + getKdfIterations: () => Promise; clear: () => Promise; isAuthenticated: () => Promise; getOrganization: (id: string) => Promise; diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index c509354f57..128a36d9ab 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -10,7 +10,6 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { ExportService } from '../../abstractions/export.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { UserService } from '../../abstractions/user.service'; export class ExportComponent { @Output() onSaved = new EventEmitter(); @@ -20,9 +19,9 @@ export class ExportComponent { showPassword = false; constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, - protected cryptoService: CryptoService, protected userService: UserService, - protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected exportService: ExportService, protected win: Window) { } + protected cryptoService: CryptoService, protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, + protected win: Window) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { @@ -31,11 +30,8 @@ export class ExportComponent { return; } - const email = await this.userService.getEmail(); - const key = await this.cryptoService.makeKey(this.masterPassword, email); - const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + 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(); diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 8fd2880000..695fa3679b 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -28,7 +28,9 @@ export class LockComponent { } const email = await this.userService.getEmail(); - const key = await this.cryptoService.makeKey(this.masterPassword, email); + const kdf = await this.userService.getKdf(); + const kdfIterations = await this.userService.getKdfIterations(); + const key = await this.cryptoService.makeKey(this.masterPassword, email, kdf, kdfIterations); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); const storedKeyHash = await this.cryptoService.getKeyHash(); diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 77f9959243..75df514fe4 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -12,6 +12,8 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; import { StateService } from '../../abstractions/state.service'; +import { KdfType } from '../../enums/kdfType'; + export class RegisterComponent { name: string = ''; email: string = ''; @@ -57,12 +59,14 @@ export class RegisterComponent { this.name = this.name === '' ? null : this.name; this.email = this.email.toLowerCase(); - const key = await this.cryptoService.makeKey(this.masterPassword, this.email); + const kdf = KdfType.PBKDF2; + const kdfIterations = 5000; + 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); + this.hint, encKey[1].encryptedString, kdf, kdfIterations); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.get('orgInvitation'); if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { diff --git a/src/enums/kdfType.ts b/src/enums/kdfType.ts new file mode 100644 index 0000000000..53eb59c4ce --- /dev/null +++ b/src/enums/kdfType.ts @@ -0,0 +1,3 @@ +export enum KdfType { + PBKDF2 = 0, +} diff --git a/src/models/request/preloginRequest.ts b/src/models/request/preloginRequest.ts new file mode 100644 index 0000000000..bee6058c7d --- /dev/null +++ b/src/models/request/preloginRequest.ts @@ -0,0 +1,7 @@ +export class PreloginRequest { + email: string; + + constructor(email: string) { + this.email = email; + } +} diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index 7d3e25fc36..d8caa4201a 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,5 +1,7 @@ import { KeysRequest } from './keysRequest'; +import { KdfType } from '../../enums/kdfType'; + export class RegisterRequest { name: string; email: string; @@ -9,12 +11,17 @@ export class RegisterRequest { keys: KeysRequest; token: string; organizationUserId: string; + kdf: KdfType; + kdfIterations: number; - constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { + constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, + kdf: KdfType, kdfIterations: number) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; this.key = key; + this.kdf = kdf; + this.kdfIterations = kdfIterations; } } diff --git a/src/models/response/preloginResponse.ts b/src/models/response/preloginResponse.ts new file mode 100644 index 0000000000..8a893b5f52 --- /dev/null +++ b/src/models/response/preloginResponse.ts @@ -0,0 +1,11 @@ +import { KdfType } from '../../enums/kdfType'; + +export class PreloginResponse { + kdf: KdfType; + kdfIterations: number; + + constructor(response: any) { + this.kdf = response.Kdf; + this.kdfIterations = response.KdfIterations; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 091c58a663..4ffdf9e71b 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -33,6 +33,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; +import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { StorageRequest } from '../models/request/storageRequest'; @@ -77,6 +78,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; +import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -196,6 +198,11 @@ export class ApiService implements ApiServiceAbstraction { return new ProfileResponse(r); } + async postPrelogin(request: PreloginRequest): Promise { + const r = await this.send('POST', '/accounts/prelogin', request, false, true); + return new PreloginResponse(r); + } + postEmailToken(request: EmailTokenRequest): Promise { return this.send('POST', '/accounts/email-token', request, true, false); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 3e2364acf0..0f991eddd9 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,3 +1,4 @@ +import { KdfType } from '../enums/kdfType'; import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; import { AuthResult } from '../models/domain/authResult'; @@ -5,8 +6,10 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { DeviceRequest } from '../models/request/deviceRequest'; import { KeysRequest } from '../models/request/keysRequest'; +import { PreloginRequest } from '../models/request/preloginRequest'; import { TokenRequest } from '../models/request/tokenRequest'; +import { ErrorResponse } from '../models/response/errorResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; @@ -77,6 +80,8 @@ export class AuthService { selectedTwoFactorProviderType: TwoFactorProviderType = null; private key: SymmetricCryptoKey; + private kdf: KdfType; + private kdfIterations: number; constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, private tokenService: TokenService, @@ -108,8 +113,7 @@ export class AuthService { async logIn(email: string, masterPassword: string): Promise { this.selectedTwoFactorProviderType = null; - email = email.toLowerCase(); - const key = await this.cryptoService.makeKey(masterPassword, email); + const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); return await this.logInHelper(email, hashedPassword, key); } @@ -123,8 +127,7 @@ export class AuthService { async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { this.selectedTwoFactorProviderType = null; - email = email.toLowerCase(); - const key = await this.cryptoService.makeKey(masterPassword, email); + const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); return await this.logInHelper(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember); } @@ -195,6 +198,24 @@ export class AuthService { return providerType; } + async makePreloginKey(masterPassword: string, email: string): Promise { + email = email.toLowerCase(); + this.kdf = null; + this.kdfIterations = null; + try { + const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); + if (preloginResponse != null) { + this.kdf = preloginResponse.kdf; + this.kdfIterations = preloginResponse.kdfIterations; + } + } catch (e) { + if (!(e instanceof ErrorResponse) || e.statusCode !== 404) { + throw e; + } + } + return this.cryptoService.makeKey(masterPassword, email, this.kdf, this.kdfIterations); + } + private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); @@ -235,7 +256,8 @@ export class AuthService { } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); - await this.userService.setUserIdAndEmail(this.tokenService.getUserId(), this.tokenService.getEmail()); + await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), + this.kdf, this.kdfIterations); if (this.setCryptoKeys) { await this.cryptoService.setKey(key); await this.cryptoService.setKeyHash(hashedPassword); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index f70359a4e6..815a152e6d 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,4 +1,5 @@ import { EncryptionType } from '../enums/encryptionType'; +import { KdfType } from '../enums/kdfType'; import { CipherString } from '../models/domain/cipherString'; import { EncryptedObject } from '../models/domain/encryptedObject'; @@ -272,8 +273,19 @@ export class CryptoService implements CryptoServiceAbstraction { await this.setKey(key); } - async makeKey(password: string, salt: string): Promise { - const key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', 5000); + async makeKey(password: string, salt: string, kdf: KdfType, kdfIterations: number): + Promise { + let key: ArrayBuffer = null; + if (kdf == null || kdf === KdfType.PBKDF2) { + if (kdfIterations == null) { + kdfIterations = 5000; + } else if (kdfIterations < 5000) { + throw new Error('PBKDF2 iteration minimum is 5000.'); + } + key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', kdfIterations); + } else { + throw new Error('Unknown Kdf.'); + } return new SymmetricCryptoKey(key); } diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 3b149643d0..df80de16a3 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -5,28 +5,37 @@ import { UserService as UserServiceAbstraction } from '../abstractions/user.serv import { OrganizationData } from '../models/data/organizationData'; import { Organization } from '../models/domain/organization'; +import { KdfType } from '../enums/kdfType'; + const Keys = { userId: 'userId', userEmail: 'userEmail', stamp: 'securityStamp', + kdf: 'kdf', + kdfIterations: 'kdfIterations', organizationsPrefix: 'organizations_', }; export class UserService implements UserServiceAbstraction { - userId: string; - email: string; - stamp: string; + private userId: string; + private email: string; + private stamp: string; + private kdf: KdfType; + private kdfIterations: number; - constructor(private tokenService: TokenService, private storageService: StorageService) { - } + constructor(private tokenService: TokenService, private storageService: StorageService) { } - setUserIdAndEmail(userId: string, email: string): Promise { + setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise { this.email = email; this.userId = userId; + this.kdf = kdf; + this.kdfIterations = kdfIterations; return Promise.all([ this.storageService.save(Keys.userEmail, email), this.storageService.save(Keys.userId, userId), + this.storageService.save(Keys.kdf, kdf), + this.storageService.save(Keys.kdfIterations, kdfIterations), ]); } @@ -62,6 +71,24 @@ export class UserService implements UserServiceAbstraction { return this.stamp; } + async getKdf(): Promise { + if (this.kdf != null) { + return this.kdf; + } + + this.kdf = await this.storageService.get(Keys.kdf); + return this.kdf; + } + + async getKdfIterations(): Promise { + if (this.kdfIterations != null) { + return this.kdfIterations; + } + + this.kdfIterations = await this.storageService.get(Keys.kdfIterations); + return this.kdfIterations; + } + async clear(): Promise { const userId = await this.getUserId(); @@ -69,10 +96,14 @@ export class UserService implements UserServiceAbstraction { this.storageService.remove(Keys.userId), this.storageService.remove(Keys.userEmail), this.storageService.remove(Keys.stamp), + this.storageService.remove(Keys.kdf), + this.storageService.remove(Keys.kdfIterations), this.clearOrganizations(userId), ]); this.userId = this.email = this.stamp = null; + this.kdf = null; + this.kdfIterations = null; } async isAuthenticated(): Promise {