From 785b681f61f81690de6df55159ab07ae710bcfad Mon Sep 17 00:00:00 2001 From: Anthony Garera Date: Fri, 3 Jul 2020 22:45:38 -0400 Subject: [PATCH 01/16] Added current date variable. This is in relation to PR #1272 in bitwarden/browser (#120) --- src/angular/components/add-edit.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 87c7cd3f8c2..a0909cb2b82 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -78,6 +78,7 @@ export class AddEditComponent implements OnInit { addFieldTypeOptions: any[]; uriMatchOptions: any[]; ownershipOptions: any[] = []; + currentDate = new Date(); protected writeableCollections: CollectionView[]; private previousCipherId: string; From d308245237fe3ddc97ba4e6c1680febd47cb9b58 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 6 Jul 2020 17:21:41 -0500 Subject: [PATCH 02/16] Added new BusinessPortal boolean to all necessary objects (#121) --- src/models/data/organizationData.ts | 2 ++ src/models/domain/organization.ts | 2 ++ src/models/response/profileOrganizationResponse.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 626691c0049..4b0fc7694c5 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -16,6 +16,7 @@ export class OrganizationData { useTotp: boolean; use2fa: boolean; useApi: boolean; + useBusinessPortal: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -35,6 +36,7 @@ export class OrganizationData { this.useTotp = response.useTotp; this.use2fa = response.use2fa; this.useApi = response.useApi; + this.useBusinessPortal = response.useBusinessPortal; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; this.seats = response.seats; diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 914f8308dfe..21011fb095d 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -16,6 +16,7 @@ export class Organization { useTotp: boolean; use2fa: boolean; useApi: boolean; + useBusinessPortal: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -39,6 +40,7 @@ export class Organization { this.useTotp = obj.useTotp; this.use2fa = obj.use2fa; this.useApi = obj.useApi; + this.useBusinessPortal = obj.useBusinessPortal; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; this.seats = obj.seats; diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 6a76878c5ae..f14ba9714ba 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -13,6 +13,7 @@ export class ProfileOrganizationResponse extends BaseResponse { useTotp: boolean; use2fa: boolean; useApi: boolean; + useBusinessPortal: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -34,6 +35,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.useTotp = this.getResponseProperty('UseTotp'); this.use2fa = this.getResponseProperty('Use2fa'); this.useApi = this.getResponseProperty('UseApi'); + this.useBusinessPortal = this.getResponseProperty('UseBusinessPortal'); this.selfHost = this.getResponseProperty('SelfHost'); this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); this.seats = this.getResponseProperty('Seats'); From 0d3b32a10d27fe642cdc90b5059fa6218e30989d Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 9 Jul 2020 15:56:09 -0500 Subject: [PATCH 03/16] Prevent malformed URLs from loading current tab --- src/services/cipher.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 7b519a1a4f1..93d5a1fbcd6 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -333,9 +333,12 @@ export class CipherService implements CipherServiceAbstraction { this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; eqDomains.forEach((eqDomain) => { - if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { - matches = matches.concat(eqDomain); + try { + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } } + catch {} }); if (!matches.length) { From ebaa69a15bc220bf87208dfc673078ea3ad08316 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 9 Jul 2020 15:59:45 -0500 Subject: [PATCH 04/16] Formatting change. Inlined catch. --- src/services/cipher.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 93d5a1fbcd6..eaf1ec0f837 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -337,8 +337,7 @@ export class CipherService implements CipherServiceAbstraction { if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { matches = matches.concat(eqDomain); } - } - catch {} + } catch {} }); if (!matches.length) { From 57649f31c4b1ef210e4070f7e031758a9de4ee19 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Fri, 10 Jul 2020 10:01:15 -0500 Subject: [PATCH 05/16] Moved error checking to utils, where parse is --- src/misc/utils.ts | 11 ++++++++--- src/services/cipher.service.ts | 8 +++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e0b70413560..e9b8b4911d4 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -204,9 +204,14 @@ export class Utils { } catch (e) { } } - const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null; - if (domain != null) { - return domain; + try { + const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null; + + if (domain != null) { + return domain; + } + } catch { + return null; } return null; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index eaf1ec0f837..7b519a1a4f1 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -333,11 +333,9 @@ export class CipherService implements CipherServiceAbstraction { this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; eqDomains.forEach((eqDomain) => { - try { - if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { - matches = matches.concat(eqDomain); - } - } catch {} + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } }); if (!matches.length) { From 58ba1ce5b6183d73c1aeb51a1fe6a7f636fdc774 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 13 Jul 2020 13:35:37 -0500 Subject: [PATCH 06/16] Modified response to include port if exists --- src/misc/utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e9b8b4911d4..119e9361ed4 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -157,7 +157,13 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { - return url != null && url.hostname !== '' ? url.hostname : null; + let hostname = url != null && url.hostname !== '' ? url.hostname : null; + + if(hostname != null && url.port !== '') { + hostname += ":" + url.port; + } + + return hostname; } catch { return null; } From 49b796ebd695dd69dee89809445dad0e454d2b1e Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 13 Jul 2020 13:39:38 -0500 Subject: [PATCH 07/16] Formatting change --- src/misc/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 119e9361ed4..806aeb5a744 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -159,7 +159,7 @@ export class Utils { try { let hostname = url != null && url.hostname !== '' ? url.hostname : null; - if(hostname != null && url.port !== '') { + if (hostname != null && url.port !== '') { hostname += ":" + url.port; } From 82230112487cc487f7298af8b567b4f7beb24988 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 13 Jul 2020 15:47:55 -0500 Subject: [PATCH 08/16] Reverted prior change. Changed call to getHost --- src/misc/utils.ts | 8 +------- src/models/view/loginUriView.ts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 806aeb5a744..e9b8b4911d4 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -157,13 +157,7 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { - let hostname = url != null && url.hostname !== '' ? url.hostname : null; - - if (hostname != null && url.port !== '') { - hostname += ":" + url.port; - } - - return hostname; + return url != null && url.hostname !== '' ? url.hostname : null; } catch { return null; } diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index afa6e200fd6..ce3dfca0eb3 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -62,7 +62,7 @@ export class LoginUriView implements View { return null; } if (this._hostname == null && this.uri != null) { - this._hostname = Utils.getHostname(this.uri); + this._hostname = Utils.getHost(this.uri); if (this._hostname === '') { this._hostname = null; } From b53046d0d9cf8a5bd4ed860b91133cafec824b73 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 14 Jul 2020 10:02:07 -0500 Subject: [PATCH 09/16] Added new _host property for consumption. --- src/models/view/loginUriView.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index ce3dfca0eb3..7ef9fa092e6 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -26,6 +26,7 @@ export class LoginUriView implements View { private _uri: string = null; private _domain: string = null; private _hostname: string = null; + private _host: string = null; private _canLaunch: boolean = null; // tslint:enable @@ -62,7 +63,7 @@ export class LoginUriView implements View { return null; } if (this._hostname == null && this.uri != null) { - this._hostname = Utils.getHost(this.uri); + this._hostname = Utils.getHostname(this.uri); if (this._hostname === '') { this._hostname = null; } @@ -71,10 +72,28 @@ export class LoginUriView implements View { return this._hostname; } + get host(): string { + if (this.match === UriMatchType.RegularExpression) { + return null; + } + if (this._host == null && this.uri != null) { + this._host = Utils.getHost(this.uri); + if (this._host === '') { + this._host = null; + } + } + + return this._host; + } + get hostnameOrUri(): string { return this.hostname != null ? this.hostname : this.uri; } + get hostOrUri(): string { + return this.host != null ? this.host : this.uri; + } + get isWebsite(): boolean { return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0 || (this.uri.indexOf('://') < 0 && Utils.tldEndingRegex.test(this.uri))); From fefef546f0f9e11377203df47de44a54809ca655 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 16 Jul 2020 08:59:29 -0400 Subject: [PATCH 10/16] sso support (#127) * support for sso * created master password boolean * resetMasterPassword flows * throw on bad ctor for token request --- src/abstractions/api.service.ts | 1 + src/abstractions/auth.service.ts | 6 ++ src/angular/components/lock.component.ts | 28 ++++++- .../components/two-factor.component.ts | 6 +- src/misc/utils.ts | 8 ++ src/models/domain/authResult.ts | 1 + src/models/request/tokenRequest.ts | 31 +++++-- src/models/response/identityTokenResponse.ts | 8 ++ src/services/api.service.ts | 4 + src/services/auth.service.ts | 80 ++++++++++++++----- src/services/constants.service.ts | 4 + 11 files changed, 148 insertions(+), 29 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 337b260b9e0..b345e74fcee 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -141,6 +141,7 @@ export abstract class ApiService { postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; + postAccountVerifyPassword: (request: PasswordVerificationRequest) => Promise; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index de7481ce603..21bba19633f 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -6,10 +6,14 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class AuthService { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string) => Promise; + logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -18,4 +22,6 @@ export abstract class AuthService { getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; makePreloginKey: (masterPassword: string, email: string) => Promise; + authingWithSso: () => boolean; + authingWithPassword: () => boolean; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 107ab471912..0cf22362c32 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,6 +1,7 @@ import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { ApiService } from '../../abstractions/api.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; @@ -16,6 +17,8 @@ import { ConstantsService } from '../../services/constants.service'; import { CipherString } from '../../models/domain/cipherString'; import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; +import { PasswordVerificationRequest } from '../../models/request/passwordVerificationRequest'; + import { Utils } from '../../misc/utils'; export class LockComponent implements OnInit { @@ -25,6 +28,7 @@ export class LockComponent implements OnInit { email: string; pinLock: boolean = false; webVaultHostname: string = ''; + formPromise: Promise; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; @@ -36,7 +40,8 @@ export class LockComponent implements OnInit { 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 environmentService: EnvironmentService, protected stateService: StateService, + protected apiService: ApiService) { } async ngOnInit() { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); @@ -98,9 +103,26 @@ export class LockComponent implements OnInit { } else { const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); - const storedKeyHash = await this.cryptoService.getKeyHash(); - if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + 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(ConstantsService.protectedPin); const encKey = await this.cryptoService.getEncKey(key); diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 9d3379405c2..94a1e1553ae 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -52,12 +52,16 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } async ngOnInit() { - if (this.authService.email == null || this.authService.masterPasswordHash == null || + if ((!this.authService.authingWithSso() && !this.authService.authingWithPassword()) || this.authService.twoFactorProvidersData == null) { this.router.navigate([this.loginRoute]); return; } + if (this.authService.authingWithSso()) { + this.successRoute = 'lock'; + } + if (this.initU2f && this.win != null && this.u2fSupported) { let customWebVaultUrl: string = null; if (this.environmentService.baseUrl != null) { diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e9b8b4911d4..4d9bf8533e0 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -89,6 +89,14 @@ export class Utils { } } + static fromBufferToUrlB64(buffer: ArrayBuffer): string { + const output = this.fromBufferToB64(buffer) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return output; + } + static fromBufferToUtf8(buffer: ArrayBuffer): string { if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(buffer).toString('utf8'); diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts index cb4bb57c657..0f5b95a521c 100644 --- a/src/models/domain/authResult.ts +++ b/src/models/domain/authResult.ts @@ -2,5 +2,6 @@ import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; export class AuthResult { twoFactor: boolean = false; + resetMasterPassword: boolean = false; twoFactorProviders: Map = null; } diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index c9bcd5ad366..f0ff7024280 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -5,15 +5,24 @@ import { DeviceRequest } from './deviceRequest'; export class TokenRequest { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + redirectUri: string; token: string; provider: TwoFactorProviderType; remember: boolean; device?: DeviceRequest; - constructor(email: string, masterPasswordHash: string, provider: TwoFactorProviderType, + constructor(credentials: string[], codes: string[], provider: TwoFactorProviderType, token: string, remember: boolean, device?: DeviceRequest) { - this.email = email; - this.masterPasswordHash = masterPasswordHash; + if (credentials != null && credentials.length > 1) { + this.email = credentials[0]; + this.masterPasswordHash = credentials[1]; + } else if (codes != null && codes.length > 2) { + this.code = codes[0]; + this.codeVerifier = codes[1]; + this.redirectUri = codes[2]; + } this.token = token; this.provider = provider; this.remember = remember; @@ -22,13 +31,23 @@ export class TokenRequest { toIdentityToken(clientId: string) { const obj: any = { - grant_type: 'password', - username: this.email, - password: this.masterPasswordHash, scope: 'api offline_access', client_id: clientId, }; + if (this.masterPasswordHash != null && this.email != null) { + obj.grant_type = 'password'; + obj.username = this.email; + obj.password = this.masterPasswordHash; + } else if (this.code != null && this.codeVerifier != null && this.redirectUri != null) { + obj.grant_type = 'authorization_code'; + obj.code = this.code; + obj.code_verifier = this.codeVerifier; + obj.redirect_uri = this.redirectUri; + } else { + throw new Error('must provide credentials or codes'); + } + if (this.device) { obj.deviceType = this.device.type; obj.deviceIdentifier = this.device.identifier; diff --git a/src/models/response/identityTokenResponse.ts b/src/models/response/identityTokenResponse.ts index 20adaff7181..7ce1afba0cf 100644 --- a/src/models/response/identityTokenResponse.ts +++ b/src/models/response/identityTokenResponse.ts @@ -1,14 +1,19 @@ import { BaseResponse } from './baseResponse'; +import { KdfType } from '../../enums/kdfType'; + export class IdentityTokenResponse extends BaseResponse { accessToken: string; expiresIn: number; refreshToken: string; tokenType: string; + resetMasterPassword: boolean; privateKey: string; key: string; twoFactorToken: string; + kdf: KdfType; + kdfIterations: number; constructor(response: any) { super(response); @@ -17,8 +22,11 @@ export class IdentityTokenResponse extends BaseResponse { this.refreshToken = response.refresh_token; this.tokenType = response.token_type; + this.resetMasterPassword = this.getResponseProperty('ResetMasterPassword'); this.privateKey = this.getResponseProperty('PrivateKey'); this.key = this.getResponseProperty('Key'); this.twoFactorToken = this.getResponseProperty('TwoFactorToken'); + this.kdf = this.getResponseProperty('Kdf'); + this.kdfIterations = this.getResponseProperty('KdfIterations'); } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f86a4efcff7..8b9249429e9 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -321,6 +321,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/verify-email-token', request, false, false); } + postAccountVerifyPassword(request: PasswordVerificationRequest): Promise { + return this.send('POST', '/accounts/verify-password', request, true, false); + } + postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { return this.send('POST', '/accounts/delete-recover', request, false, false); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ad379f29fd4..9a8d0ec3e0c 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -77,12 +77,13 @@ export const TwoFactorProviders = { export class AuthService implements AuthServiceAbstraction { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; twoFactorProvidersData: Map; 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, @@ -116,13 +117,19 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, key); + return await this.logInHelper(email, hashedPassword, null, null, null, key, + null, null, null); + } + + async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, null, null, null); } async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { - return await this.logInHelper(this.email, this.masterPasswordHash, this.key, twoFactorProvider, - twoFactorToken, remember); + return await this.logInHelper(this.email, this.masterPasswordHash, this.code, this.codeVerifier, + this.ssoRedirectUrl, this.key, twoFactorProvider, twoFactorToken, remember); } async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -130,7 +137,8 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; 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); + return await this.logInHelper(email, hashedPassword, null, null, null, key, twoFactorProvider, twoFactorToken, + remember); } logOut(callback: Function) { @@ -201,37 +209,59 @@ export class AuthService implements AuthServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - this.kdf = null; - this.kdfIterations = null; + let kdf: KdfType = null; + let kdfIterations: number = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - this.kdf = preloginResponse.kdf; - this.kdfIterations = preloginResponse.kdfIterations; + kdf = preloginResponse.kdf; + kdfIterations = preloginResponse.kdfIterations; } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return this.cryptoService.makeKey(masterPassword, email, this.kdf, this.kdfIterations); + return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); } - private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, - twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { + authingWithSso(): boolean { + return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; + } + + authingWithPassword(): boolean { + return this.email != null && this.masterPasswordHash != null; + } + + private async logInHelper(email: string, hashedPassword: string, code: string, codeVerifier: string, + redirectUrl: string, key: SymmetricCryptoKey, twoFactorProvider?: TwoFactorProviderType, + twoFactorToken?: string, remember?: boolean): Promise { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const appId = await this.appIdService.getAppId(); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); + let emailPassword: string[] = []; + let codeCodeVerifier: string[] = []; + if (email != null && hashedPassword != null) { + emailPassword = [email, hashedPassword]; + } else { + emailPassword = null; + } + if (code != null && codeVerifier != null && redirectUrl != null) { + codeCodeVerifier = [code, codeVerifier, redirectUrl]; + } else { + codeCodeVerifier = null; + } + let request: TokenRequest; if (twoFactorToken != null && twoFactorProvider != null) { - request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember, deviceRequest); } else if (storedTwoFactorToken != null) { - request = new TokenRequest(email, hashedPassword, TwoFactorProviderType.Remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember, storedTwoFactorToken, false, deviceRequest); } else { - request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); + request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest); } const response = await this.apiService.postIdentityToken(request); @@ -245,6 +275,9 @@ export class AuthService implements AuthServiceAbstraction { const twoFactorResponse = response as IdentityTwoFactorResponse; this.email = email; this.masterPasswordHash = hashedPassword; + this.code = code; + this.codeVerifier = codeVerifier; + this.ssoRedirectUrl = redirectUrl; this.key = this.setCryptoKeys ? key : null; this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; @@ -252,16 +285,21 @@ export class AuthService implements AuthServiceAbstraction { } const tokenResponse = response as IdentityTokenResponse; + result.resetMasterPassword = tokenResponse.resetMasterPassword; if (tokenResponse.twoFactorToken != null) { await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), - this.kdf, this.kdfIterations); + tokenResponse.kdf, tokenResponse.kdfIterations); if (this.setCryptoKeys) { - await this.cryptoService.setKey(key); - await this.cryptoService.setKeyHash(hashedPassword); + if (key != null) { + await this.cryptoService.setKey(key); + } + if (hashedPassword != null) { + await this.cryptoService.setKeyHash(hashedPassword); + } await this.cryptoService.setEncKey(tokenResponse.key); // User doesn't have a key pair yet (old account), let's generate one for them @@ -284,8 +322,12 @@ export class AuthService implements AuthServiceAbstraction { } private clearState(): void { + this.key = null; this.email = null; this.masterPasswordHash = null; + this.code = null; + this.codeVerifier = null; + this.ssoRedirectUrl = null; this.twoFactorProvidersData = null; this.selectedTwoFactorProviderType = null; } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 396ebffa3fa..752648fd795 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -23,6 +23,8 @@ export class ConstantsService { static readonly protectedPin: string = 'protectedPin'; static readonly clearClipboardKey: string = 'clearClipboardKey'; static readonly eventCollectionKey: string = 'eventCollection'; + static readonly ssoCodeVerifierKey: string = 'ssoCodeVerifier'; + static readonly ssoStateKey: string = 'ssoState'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -47,4 +49,6 @@ export class ConstantsService { readonly protectedPin: string = ConstantsService.protectedPin; readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; + readonly ssoCodeVerifierKey: string = ConstantsService.ssoCodeVerifierKey; + readonly ssoStateKey: string = ConstantsService.ssoStateKey; } From b95e7aeb6229fe388150663c3b7f6a898ddf176b Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:05:20 -0400 Subject: [PATCH 11/16] Updated contribution guidance for developers --- CONTRIBUTING.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 210e7f51208..ec7f6e2f9a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,62 @@ Code contributions are welcome! Please commit any pull requests against the `master` branch. + +# Setting up your Local Dev environment for jslib +In order to easily test, check and develop against local changes to jslib across each of the TypeScript/JavaScript clients it is recommended to use symlinks for the submodule so that you only have to make the change once and don't need to x-copy or wait for a commit+merge to checkout, pull and test against your other repos. + +## Prerequisites +1. git bash or other git command line + +## Clone Repos +In order for this to work well, you need to use a consistent relative directory structure. Repos should be cloned in the following way: + +* `./`; we'll call this `/dev` ('cause why not) + * jslib - `git clone https://github.com/bitwarden/jslib.git` (/dev/jslib) + * web - `git clone --recurse-submodules https://github.com/bitwarden/web.git` (/dev/web) + * desktop - `git clone --recurse-submodules https://github.com/bitwarden/desktop.git` (/dev/desktop) + * browser - `git clone --recurse-submodules https://github.com/bitwarden/browser.git` (/dev/browser) + * cli - `git clone --recurse-submodules https://github.com/bitwarden/cli` (/dev/cli) + +You should notice web, desktop, browser and cli each reference jslib as a git submodule. If you've already cloned the repos but didn't use `--recurse-submodules` then you'll need to init those: + +`npm run sub:init` + +## Configure Symlinks +Be aware that using git clone will make symlinks added to your repo be seen by git as plain text file paths, lets make sure this is set to true to prevent that. In the project root run, `git config core.symlinks true`. + +For each project other than jslib, run the following: + +For macOS/Linux: `npm run symlink:mac` + +For Windows: `npm run symlink:win` + +## Updates and Cleanup +* Need to update parent repo that has jslib as a submodule to latest from actual jslib repo? + * Create branch from master (`git checkout -b update-jslib`) + * From new local branch: `npm run sub:pull` (`git submodule foreach git pull origin master`) + * Follow Pull Request notes for commit/push instructions + * Once merged, pull master, rebase your feature branch and then do npm run sub:update to catch your submodule up +* Discard changes made to a submodule + * `git submodule foreach git reset —hard` + + +## Merge Conflicts +At times when you need to perform a `git merge master` into your feature or local branch, and there are conflicting version references to the *jslib* repo from your other clients, you will not be able to use the traditional merge or stage functions you would normally use for a file. + +To resolve you must use either `git reset` or update the index directly using `git update-index`. You can use (depending on whether you have symlink'd jslib) one of the following: + +```bash +git reset master -- jslib +git reset master@{upstream} -- jslib +git reset HEAD -- jslib +git reset MERGE_HEAD -- jslib +``` + +Those should automatically stage the change and reset the jslib submodule back to where it needs to be (generally at the latest version from `master`). + +The other option is to update the index directly using the plumbing command git update-index. To do that, you need to know that an object of type gitlink (i.e., directory entry in a parent repository that points to a submodule) is 0160000. You can figure it out from `git ls-files -s` or the following reference (see "1110 (gitlink)" under 4-bit object type): https://github.com/gitster/git/blob/master/Documentation/technical/index-format.txt + +To use that approach, figure out the hash you want to set the submodule to, then run, e.g.: + +`git update-index --cacheinfo 0160000,533da4ea00703f4ad6d5518e1ce81d20261c40c0,jslib` + +see: [https://stackoverflow.com/questions/26617838/how-to-resolve-git-submodule-conflict-if-submodule-is-not-initialized](https://stackoverflow.com/questions/26617838/how-to-resolve-git-submodule-conflict-if-submodule-is-not-initialized) From 9ca79c49294ecbea10f68220d0da87dd9bf15f3f Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 17 Jul 2020 16:05:58 -0400 Subject: [PATCH 12/16] Reference id to data conversion --- src/angular/components/register.component.ts | 5 +++-- src/models/domain/referenceEventData.ts | 5 +++++ src/models/request/registerRequest.ts | 7 ++++--- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 src/models/domain/referenceEventData.ts diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index b3ad95d377d..29a9ee04ed2 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -12,6 +12,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; +import { ReferenceEventData } from '../../models/domain/referenceEventData'; export class RegisterComponent { name: string = ''; @@ -22,7 +23,7 @@ export class RegisterComponent { showPassword: boolean = false; formPromise: Promise; masterPasswordScore: number; - referenceId: string; + referenceData: ReferenceEventData; protected successRoute = 'login'; private masterPasswordStrengthTimeout: any; @@ -111,7 +112,7 @@ export class RegisterComponent { 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.referenceId); + this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData); 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/models/domain/referenceEventData.ts b/src/models/domain/referenceEventData.ts new file mode 100644 index 00000000000..78ffcbfaf5a --- /dev/null +++ b/src/models/domain/referenceEventData.ts @@ -0,0 +1,5 @@ +export class ReferenceEventData { + id: string; + layout: string; + flow: string; +} diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index f96544e80e6..ebe998435a6 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,6 +1,7 @@ import { KeysRequest } from './keysRequest'; import { KdfType } from '../../enums/kdfType'; +import { ReferenceEventData } from '../domain/referenceEventData'; export class RegisterRequest { name: string; @@ -13,10 +14,10 @@ export class RegisterRequest { organizationUserId: string; kdf: KdfType; kdfIterations: number; - referenceId: string; + referenceData: ReferenceEventData; constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, - kdf: KdfType, kdfIterations: number, referenceId: string) { + kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventData) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; @@ -24,6 +25,6 @@ export class RegisterRequest { this.key = key; this.kdf = kdf; this.kdfIterations = kdfIterations; - this.referenceId = referenceId; + this.referenceData = referenceData; } } From b599c2e74f32534dd85dc94593d38c18f2414842 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Jul 2020 14:20:32 -0400 Subject: [PATCH 13/16] support url header for firefox import (#132) --- src/importers/firefoxCsvImporter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/importers/firefoxCsvImporter.ts b/src/importers/firefoxCsvImporter.ts index 8c5b1c1313c..a7d6a5b356c 100644 --- a/src/importers/firefoxCsvImporter.ts +++ b/src/importers/firefoxCsvImporter.ts @@ -14,10 +14,11 @@ export class FirefoxCsvImporter extends BaseImporter implements Importer { results.forEach((value) => { const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(this.nameFromUrl(value.hostname), '--'); + const url = this.getValueOrDefault(value.url, this.getValueOrDefault(value.hostname)); + cipher.name = this.getValueOrDefault(this.nameFromUrl(url), '--'); cipher.login.username = this.getValueOrDefault(value.username); cipher.login.password = this.getValueOrDefault(value.password); - cipher.login.uris = this.makeUriArray(value.hostname); + cipher.login.uris = this.makeUriArray(url); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); From 7771b2293dbfc08fc867bb18d3c84bfe88d67ee2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Jul 2020 15:00:33 -0400 Subject: [PATCH 14/16] parse otp for keepass import (#133) --- src/importers/keepass2XmlImporter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 1ad213e2388..a485d2265e9 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -68,6 +68,8 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { cipher.login.username = value; } else if (key === 'Password') { cipher.login.password = value; + } else if (key === 'otp') { + cipher.login.totp = value.replace('key=', ''); } else if (key === 'Title') { cipher.name = value; } else if (key === 'Notes') { From 97d24f5abffccd657655c17da05ca932649c9d13 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:21:01 -0400 Subject: [PATCH 15/16] reference event data model changes --- src/angular/components/register.component.ts | 4 ++-- .../referenceEventRequest.ts} | 2 +- src/models/request/registerRequest.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/models/{domain/referenceEventData.ts => request/referenceEventRequest.ts} (60%) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 29a9ee04ed2..0ba73829d20 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -12,7 +12,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; -import { ReferenceEventData } from '../../models/domain/referenceEventData'; +import { ReferenceEventRequest } from '../../models/request/referenceEventRequest'; export class RegisterComponent { name: string = ''; @@ -23,7 +23,7 @@ export class RegisterComponent { showPassword: boolean = false; formPromise: Promise; masterPasswordScore: number; - referenceData: ReferenceEventData; + referenceData: ReferenceEventRequest; protected successRoute = 'login'; private masterPasswordStrengthTimeout: any; diff --git a/src/models/domain/referenceEventData.ts b/src/models/request/referenceEventRequest.ts similarity index 60% rename from src/models/domain/referenceEventData.ts rename to src/models/request/referenceEventRequest.ts index 78ffcbfaf5a..4cd8c507f65 100644 --- a/src/models/domain/referenceEventData.ts +++ b/src/models/request/referenceEventRequest.ts @@ -1,4 +1,4 @@ -export class ReferenceEventData { +export class ReferenceEventRequest { id: string; layout: string; flow: string; diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index ebe998435a6..a93ac69bba8 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,7 +1,7 @@ import { KeysRequest } from './keysRequest'; +import { ReferenceEventRequest } from './referenceEventRequest'; import { KdfType } from '../../enums/kdfType'; -import { ReferenceEventData } from '../domain/referenceEventData'; export class RegisterRequest { name: string; @@ -14,10 +14,10 @@ export class RegisterRequest { organizationUserId: string; kdf: KdfType; kdfIterations: number; - referenceData: ReferenceEventData; + referenceData: ReferenceEventRequest; constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, - kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventData) { + kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventRequest) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; From 6e79dfa01a785fb239c7f0026dcd427db981eb55 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:38:56 -0400 Subject: [PATCH 16/16] fixed import groupings/order --- src/angular/components/register.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 0ba73829d20..b3f689fcd0c 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -1,6 +1,7 @@ import { Router } from '@angular/router'; import { KeysRequest } from '../../models/request/keysRequest'; +import { ReferenceEventRequest } from '../../models/request/referenceEventRequest'; import { RegisterRequest } from '../../models/request/registerRequest'; import { ApiService } from '../../abstractions/api.service'; @@ -12,7 +13,6 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; -import { ReferenceEventRequest } from '../../models/request/referenceEventRequest'; export class RegisterComponent { name: string = '';