diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..b2c2a6a4 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +193434461dbd9c48fe5dcbad95693470aec422ac diff --git a/README.md b/README.md index fed8186b..ce5cb1f1 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,23 @@ Common code referenced across Bitwarden JavaScript projects. - _Microsoft Build Tools 2015_ in Visual Studio Installer - [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) either by downloading it seperately or through the Visual Studio Installer. + +## Prettier + +We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps: + +1. Check out your local Branch +2. Run `git merge 8b2dfc6cdcb8ff5b604364c2ea6d343473aee7cd` +3. Resolve any merge conflicts, commit. +4. Run `npm run prettier` +5. Commit +6. Run `git merge -Xours 193434461dbd9c48fe5dcbad95693470aec422ac` +7. Push + +### Git blame + +We also recommend that you configure git to ignore the prettier revision using: + +```bash +git config blame.ignoreRevsFile .git-blame-ignore-revs +``` diff --git a/angular/src/components/change-password.component.ts b/angular/src/components/change-password.component.ts index 6ca81494..2431ece2 100644 --- a/angular/src/components/change-password.component.ts +++ b/angular/src/components/change-password.component.ts @@ -40,7 +40,7 @@ export class ChangePasswordComponent implements OnInit { async ngOnInit() { this.email = await this.stateService.getEmail(); - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + this.enforcedPolicyOptions ??= await this.policyService.getMasterPasswordPolicyOptions(); } async submit() { diff --git a/angular/src/components/export.component.ts b/angular/src/components/export.component.ts index f3aba8e2..17a751fc 100644 --- a/angular/src/components/export.component.ts +++ b/angular/src/components/export.component.ts @@ -20,7 +20,7 @@ export class ExportComponent implements OnInit { formPromise: Promise; disabledByPolicy: boolean = false; - exportForm = this.fb.group({ + exportForm = this.formBuilder.group({ format: ["json"], secret: [""], }); @@ -41,7 +41,7 @@ export class ExportComponent implements OnInit { protected win: Window, private logService: LogService, private userVerificationService: UserVerificationService, - private fb: FormBuilder + private formBuilder: FormBuilder ) {} async ngOnInit() { diff --git a/angular/src/components/icon.component.ts b/angular/src/components/icon.component.ts index a32948cb..d30ce20e 100644 --- a/angular/src/components/icon.component.ts +++ b/angular/src/components/icon.component.ts @@ -18,6 +18,21 @@ const IconMap: any = { "fa-apple": String.fromCharCode(0xf179), }; +/** + * Provides a mapping from supported card brands to + * the filenames of icon that should be present in images/cards folder of clients. + */ +const cardIcons: Record = { + Visa: "card-visa", + Mastercard: "card-mastercard", + Amex: "card-amex", + Discover: "card-discover", + "Diners Club": "card-diners-club", + JCB: "card-jcb", + Maestro: "card-maestro", + UnionPay: "card-union-pay", +}; + @Component({ selector: "app-vault-icon", templateUrl: "icon.component.html", @@ -59,6 +74,7 @@ export class IconComponent implements OnChanges { break; case CipherType.Card: this.icon = "fa-credit-card"; + this.setCardIcon(); break; case CipherType.Identity: this.icon = "fa-id-card-o"; @@ -102,4 +118,11 @@ export class IconComponent implements OnChanges { this.image = null; } } + + private setCardIcon() { + const brand = this.cipher.card.brand; + if (this.imageEnabled && brand in cardIcons) { + this.icon = "credit-card-icon " + cardIcons[brand]; + } + } } diff --git a/angular/src/components/set-password.component.ts b/angular/src/components/set-password.component.ts index e651eca5..89f6e520 100644 --- a/angular/src/components/set-password.component.ts +++ b/angular/src/components/set-password.component.ts @@ -79,6 +79,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { const response = await this.apiService.getOrganizationAutoEnrollStatus(this.identifier); this.orgId = response.id; this.resetPasswordAutoEnroll = response.resetPasswordEnabled; + this.enforcedPolicyOptions = + await this.policyService.getMasterPasswordPoliciesForInvitedUsers(this.orgId); } catch { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } diff --git a/angular/src/components/settings/vault-timeout-input.component.ts b/angular/src/components/settings/vault-timeout-input.component.ts index 95180f88..2f63f97f 100644 --- a/angular/src/components/settings/vault-timeout-input.component.ts +++ b/angular/src/components/settings/vault-timeout-input.component.ts @@ -21,9 +21,9 @@ export class VaultTimeoutInputComponent implements ControlValueAccessor, Validat static CUSTOM_VALUE = -100; - form = this.fb.group({ + form = this.formBuilder.group({ vaultTimeout: [null], - custom: this.fb.group({ + custom: this.formBuilder.group({ hours: [null], minutes: [null], }), @@ -38,7 +38,7 @@ export class VaultTimeoutInputComponent implements ControlValueAccessor, Validat private validatorChange: () => void; constructor( - private fb: FormBuilder, + private formBuilder: FormBuilder, private policyService: PolicyService, private i18nService: I18nService ) {} diff --git a/angular/src/images/cards/amex-dark.png b/angular/src/images/cards/amex-dark.png new file mode 100644 index 00000000..ac1b0759 Binary files /dev/null and b/angular/src/images/cards/amex-dark.png differ diff --git a/angular/src/images/cards/amex-light.png b/angular/src/images/cards/amex-light.png new file mode 100644 index 00000000..ac1b0759 Binary files /dev/null and b/angular/src/images/cards/amex-light.png differ diff --git a/angular/src/images/cards/diners_club-dark.png b/angular/src/images/cards/diners_club-dark.png new file mode 100644 index 00000000..a5d23233 Binary files /dev/null and b/angular/src/images/cards/diners_club-dark.png differ diff --git a/angular/src/images/cards/diners_club-light.png b/angular/src/images/cards/diners_club-light.png new file mode 100644 index 00000000..e2e33a24 Binary files /dev/null and b/angular/src/images/cards/diners_club-light.png differ diff --git a/angular/src/images/cards/discover-dark.png b/angular/src/images/cards/discover-dark.png new file mode 100644 index 00000000..2ed6f6f9 Binary files /dev/null and b/angular/src/images/cards/discover-dark.png differ diff --git a/angular/src/images/cards/discover-light.png b/angular/src/images/cards/discover-light.png new file mode 100644 index 00000000..755c6957 Binary files /dev/null and b/angular/src/images/cards/discover-light.png differ diff --git a/angular/src/images/cards/jcb-dark.png b/angular/src/images/cards/jcb-dark.png new file mode 100644 index 00000000..1cb484d9 Binary files /dev/null and b/angular/src/images/cards/jcb-dark.png differ diff --git a/angular/src/images/cards/jcb-light.png b/angular/src/images/cards/jcb-light.png new file mode 100644 index 00000000..2b89f5b9 Binary files /dev/null and b/angular/src/images/cards/jcb-light.png differ diff --git a/angular/src/images/cards/maestro-dark.png b/angular/src/images/cards/maestro-dark.png new file mode 100644 index 00000000..4a4879b5 Binary files /dev/null and b/angular/src/images/cards/maestro-dark.png differ diff --git a/angular/src/images/cards/maestro-light.png b/angular/src/images/cards/maestro-light.png new file mode 100644 index 00000000..2329738a Binary files /dev/null and b/angular/src/images/cards/maestro-light.png differ diff --git a/angular/src/images/cards/mastercard-dark.png b/angular/src/images/cards/mastercard-dark.png new file mode 100644 index 00000000..cd806263 Binary files /dev/null and b/angular/src/images/cards/mastercard-dark.png differ diff --git a/angular/src/images/cards/mastercard-light.png b/angular/src/images/cards/mastercard-light.png new file mode 100644 index 00000000..f87ae259 Binary files /dev/null and b/angular/src/images/cards/mastercard-light.png differ diff --git a/angular/src/images/cards/union_pay-dark.png b/angular/src/images/cards/union_pay-dark.png new file mode 100644 index 00000000..248be7c3 Binary files /dev/null and b/angular/src/images/cards/union_pay-dark.png differ diff --git a/angular/src/images/cards/union_pay-light.png b/angular/src/images/cards/union_pay-light.png new file mode 100644 index 00000000..2d75aa90 Binary files /dev/null and b/angular/src/images/cards/union_pay-light.png differ diff --git a/angular/src/images/cards/visa-dark.png b/angular/src/images/cards/visa-dark.png new file mode 100644 index 00000000..0705b68e Binary files /dev/null and b/angular/src/images/cards/visa-dark.png differ diff --git a/angular/src/images/cards/visa-light.png b/angular/src/images/cards/visa-light.png new file mode 100644 index 00000000..3dc8ae50 Binary files /dev/null and b/angular/src/images/cards/visa-light.png differ diff --git a/angular/src/scss/icons.scss b/angular/src/scss/icons.scss new file mode 100644 index 00000000..4d2ea459 --- /dev/null +++ b/angular/src/scss/icons.scss @@ -0,0 +1,44 @@ +$card-icons-base: "~@bitwarden/jslib-angular/src/images/cards/"; +$card-icons: ( + "visa": $card-icons-base + "visa-light.png", + "amex": $card-icons-base + "amex-light.png", + "diners-club": $card-icons-base + "diners_club-light.png", + "discover": $card-icons-base + "discover-light.png", + "jcb": $card-icons-base + "jcb-light.png", + "maestro": $card-icons-base + "maestro-light.png", + "mastercard": $card-icons-base + "mastercard-light.png", + "union-pay": $card-icons-base + "union_pay-light.png", +); + +$card-icons-dark: ( + "visa": $card-icons-base + "visa-dark.png", + "amex": $card-icons-base + "amex-dark.png", + "diners-club": $card-icons-base + "diners_club-dark.png", + "discover": $card-icons-base + "discover-dark.png", + "jcb": $card-icons-base + "jcb-dark.png", + "maestro": $card-icons-base + "maestro-dark.png", + "mastercard": $card-icons-base + "mastercard-dark.png", + "union-pay": $card-icons-base + "union_pay-dark.png", +); + +.credit-card-icon { + display: block; // Resolves the parent container being slighly to big + height: 19px; + width: 24px; + background-size: contain; + background-repeat: no-repeat; +} + +@each $name, $url in $card-icons { + .card-#{$name} { + background-image: url("#{$url}"); + } +} + +@each $theme in $dark-icon-themes { + @each $name, $url in $card-icons-dark { + .#{$theme} .card-#{$name} { + background-image: url("#{$url}"); + } + } +} diff --git a/common/spec/importers/onepassword1PifImporter.spec.ts b/common/spec/importers/onepassword1PifImporter.spec.ts index b2c0a5c6..b52c921f 100644 --- a/common/spec/importers/onepassword1PifImporter.spec.ts +++ b/common/spec/importers/onepassword1PifImporter.spec.ts @@ -476,6 +476,19 @@ describe("1Password 1Pif Importer", () => { // remaining fields as custom fields expect(cipher.fields.length).toEqual(6); + const fields = cipher.fields; + expect(fields[0].name).toEqual("sex"); + expect(fields[0].value).toEqual("male"); + expect(fields[1].name).toEqual("birth date"); + expect(fields[1].value).toEqual("Mon, 11 Mar 2019 12:01:00 GMT"); + expect(fields[2].name).toEqual("occupation"); + expect(fields[2].value).toEqual("Engineer"); + expect(fields[3].name).toEqual("department"); + expect(fields[3].value).toEqual("IT"); + expect(fields[4].name).toEqual("job title"); + expect(fields[4].value).toEqual("Developer"); + expect(fields[5].name).toEqual("home"); + expect(fields[5].value).toEqual("+49 333 222 111"); }); it("should create password history", async () => { diff --git a/common/src/abstractions/api.service.ts b/common/src/abstractions/api.service.ts index ca648f0c..4ef3c1a9 100644 --- a/common/src/abstractions/api.service.ts +++ b/common/src/abstractions/api.service.ts @@ -355,6 +355,10 @@ export abstract class ApiService { email: string, organizationUserId: string ) => Promise>; + getPoliciesByInvitedUser: ( + organizationId: string, + userId: string + ) => Promise>; putPolicy: ( organizationId: string, type: PolicyType, diff --git a/common/src/abstractions/policy.service.ts b/common/src/abstractions/policy.service.ts index 97dcb4ad..f1f6b4e5 100644 --- a/common/src/abstractions/policy.service.ts +++ b/common/src/abstractions/policy.service.ts @@ -15,6 +15,7 @@ export abstract class PolicyService { getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise; replace: (policies: { [id: string]: PolicyData }) => Promise; clear: (userId?: string) => Promise; + getMasterPasswordPoliciesForInvitedUsers: (orgId: string) => Promise; getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise; evaluateMasterPassword: ( passwordStrength: number, diff --git a/common/src/abstractions/state.service.ts b/common/src/abstractions/state.service.ts index 55707b7f..5dd3da2e 100644 --- a/common/src/abstractions/state.service.ts +++ b/common/src/abstractions/state.service.ts @@ -24,11 +24,11 @@ import { CollectionView } from "../models/view/collectionView"; import { FolderView } from "../models/view/folderView"; import { SendView } from "../models/view/sendView"; -export abstract class StateService { - accounts: BehaviorSubject<{ [userId: string]: Account }>; +export abstract class StateService { + accounts: BehaviorSubject<{ [userId: string]: T }>; activeAccount: BehaviorSubject; - addAccount: (account: Account) => Promise; + addAccount: (account: T) => Promise; setActiveUser: (userId: string) => Promise; clean: (options?: StorageOptions) => Promise; init: () => Promise; diff --git a/common/src/enums/stateVersion.ts b/common/src/enums/stateVersion.ts new file mode 100644 index 00000000..aa45edbb --- /dev/null +++ b/common/src/enums/stateVersion.ts @@ -0,0 +1,5 @@ +export enum StateVersion { + One = 1, // Original flat key/value pair store + Two = 2, // Move to a typed State object + Latest = Two, +} diff --git a/common/src/importers/onepasswordImporters/onepassword1PifImporter.ts b/common/src/importers/onepasswordImporters/onepassword1PifImporter.ts index 01ec4e59..fd202ba5 100644 --- a/common/src/importers/onepasswordImporters/onepassword1PifImporter.ts +++ b/common/src/importers/onepasswordImporters/onepassword1PifImporter.ts @@ -172,7 +172,11 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { return; } - const fieldValue = field[valueKey].toString(); + // TODO: when date FieldType exists, store this as a date field type instead of formatted Text if k is 'date' + const fieldValue = + field.k === "date" + ? new Date(field[valueKey] * 1000).toUTCString() + : field[valueKey].toString(); const fieldDesignation = field[designationKey] != null ? field[designationKey].toString() : null; diff --git a/common/src/misc/utils.ts b/common/src/misc/utils.ts index b39ada3b..a91ccd5a 100644 --- a/common/src/misc/utils.ts +++ b/common/src/misc/utils.ts @@ -7,7 +7,6 @@ const nodeURL = typeof window === "undefined" ? require("url") : null; export class Utils { static inited = false; - static isNativeScript = false; static isNode = false; static isBrowser = true; static isMobileBrowser = false; @@ -30,18 +29,13 @@ export class Utils { (process as any).release != null && (process as any).release.name === "node"; Utils.isBrowser = typeof window !== "undefined"; - Utils.isNativeScript = !Utils.isNode && !Utils.isBrowser; Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window); Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window); - Utils.global = Utils.isNativeScript - ? global - : Utils.isNode && !Utils.isBrowser - ? global - : window; + Utils.global = Utils.isNode && !Utils.isBrowser ? global : window; } static fromB64ToArray(str: string): Uint8Array { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return new Uint8Array(Buffer.from(str, "base64")); } else { const binaryString = window.atob(str); @@ -58,7 +52,7 @@ export class Utils { } static fromHexToArray(str: string): Uint8Array { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return new Uint8Array(Buffer.from(str, "hex")); } else { const bytes = new Uint8Array(str.length / 2); @@ -70,7 +64,7 @@ export class Utils { } static fromUtf8ToArray(str: string): Uint8Array { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return new Uint8Array(Buffer.from(str, "utf8")); } else { const strUtf8 = unescape(encodeURIComponent(str)); @@ -91,7 +85,7 @@ export class Utils { } static fromBufferToB64(buffer: ArrayBuffer): string { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return Buffer.from(buffer).toString("base64"); } else { let binary = ""; @@ -112,7 +106,7 @@ export class Utils { } static fromBufferToUtf8(buffer: ArrayBuffer): string { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return Buffer.from(buffer).toString("utf8"); } else { const bytes = new Uint8Array(buffer); @@ -127,7 +121,7 @@ export class Utils { // ref: https://stackoverflow.com/a/40031979/1090359 static fromBufferToHex(buffer: ArrayBuffer): string { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return Buffer.from(buffer).toString("hex"); } else { const bytes = new Uint8Array(buffer); @@ -160,7 +154,7 @@ export class Utils { } static fromUtf8ToB64(utfStr: string): string { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return Buffer.from(utfStr, "utf8").toString("base64"); } else { return decodeURIComponent(escape(window.btoa(utfStr))); @@ -172,7 +166,7 @@ export class Utils { } static fromB64ToUtf8(b64Str: string): string { - if (Utils.isNode || Utils.isNativeScript) { + if (Utils.isNode) { return Buffer.from(b64Str, "base64").toString("utf8"); } else { return decodeURIComponent(escape(window.atob(b64Str))); @@ -387,7 +381,7 @@ export class Utils { private static getUrlObject(uriString: string): URL { try { if (nodeURL != null) { - return nodeURL.URL ? new nodeURL.URL(uriString) : nodeURL.parse(uriString); + return new nodeURL.URL(uriString); } else if (typeof URL === "function") { return new URL(uriString); } else if (window != null) { diff --git a/common/src/models/domain/account.ts b/common/src/models/domain/account.ts index 22ab5ddf..6a998138 100644 --- a/common/src/models/domain/account.ts +++ b/common/src/models/domain/account.ts @@ -95,9 +95,6 @@ export class AccountProfile { hasPremiumPersonally?: boolean; lastActive?: number; lastSync?: string; - ssoCodeVerifier?: string; - ssoOrganizationIdentifier?: string; - ssoState?: string; userId?: string; usesKeyConnector?: boolean; keyHash?: string; @@ -133,9 +130,6 @@ export class AccountSettings { enableMinimizeToTray?: boolean; enableStartToTray?: boolean; enableTray?: boolean; - environmentUrls?: any = { - server: "bitwarden.com", - }; equivalentDomains?: any; minimizeOnCopyToClipboard?: boolean; neverDomains?: { [id: string]: any }; @@ -145,7 +139,7 @@ export class AccountSettings { protectedPin?: string; settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly vaultTimeout?: number; - vaultTimeoutAction?: string; + vaultTimeoutAction?: string = "lock"; } export class AccountTokens { diff --git a/common/src/models/domain/globalState.ts b/common/src/models/domain/globalState.ts index ad270bce..0dcf4939 100644 --- a/common/src/models/domain/globalState.ts +++ b/common/src/models/domain/globalState.ts @@ -1,12 +1,17 @@ +import { StateVersion } from "../../enums/stateVersion"; + export class GlobalState { enableAlwaysOnTop?: boolean; installedVersion?: string; lastActive?: number; - locale?: string; + locale?: string = "en"; openAtLogin?: boolean; organizationInvitation?: any; + ssoCodeVerifier?: string; + ssoOrganizationIdentifier?: string; + ssoState?: string; rememberedEmail?: string; - theme?: string; + theme?: string = "light"; window?: Map = new Map(); twoFactorToken?: string; disableFavicon?: boolean; @@ -20,5 +25,8 @@ export class GlobalState { biometricText?: string; noAutoPromptBiometrics?: boolean; noAutoPromptBiometricsText?: string; - stateVersion: number; + stateVersion: StateVersion = StateVersion.Latest; + environmentUrls?: any = { + server: "bitwarden.com", + }; } diff --git a/common/src/models/domain/state.ts b/common/src/models/domain/state.ts index 17889bf4..d9087264 100644 --- a/common/src/models/domain/state.ts +++ b/common/src/models/domain/state.ts @@ -1,8 +1,8 @@ import { Account } from "./account"; import { GlobalState } from "./globalState"; -export class State { - accounts: { [userId: string]: Account } = {}; +export class State { + accounts: { [userId: string]: TAccount } = {}; globals: GlobalState = new GlobalState(); activeUserId: string; } diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index 2764108e..16b94b09 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -1047,6 +1047,20 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PolicyResponse); } + async getPoliciesByInvitedUser( + organizationId: string, + userId: string + ): Promise> { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/policies/invited-user?" + "userId=" + userId, + null, + false, + true + ); + return new ListResponse(r, PolicyResponse); + } + async putPolicy( organizationId: string, type: PolicyType, diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 5e44c90d..3d7506d3 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -2,7 +2,13 @@ import { HashPurpose } from "../enums/hashPurpose"; import { KdfType } from "../enums/kdfType"; import { TwoFactorProviderType } from "../enums/twoFactorProviderType"; -import { Account, AccountData, AccountProfile, AccountTokens } from "../models/domain/account"; +import { + Account, + AccountData, + AccountKeys, + AccountProfile, + AccountTokens, +} from "../models/domain/account"; import { AuthResult } from "../models/domain/authResult"; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; @@ -538,27 +544,34 @@ export class AuthService implements AuthServiceAbstraction { result.forcePasswordReset = tokenResponse.forcePasswordReset; const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken); - await this.stateService.addAccount({ - profile: { - ...new AccountProfile(), - ...{ - userId: accountInformation.sub, - email: accountInformation.email, - apiKeyClientId: clientId, - apiKeyClientSecret: clientSecret, - hasPremiumPersonally: accountInformation.premium, - kdfIterations: tokenResponse.kdfIterations, - kdfType: tokenResponse.kdf, + await this.stateService.addAccount( + new Account({ + profile: { + ...new AccountProfile(), + ...{ + userId: accountInformation.sub, + email: accountInformation.email, + apiKeyClientId: clientId, + hasPremiumPersonally: accountInformation.premium, + kdfIterations: tokenResponse.kdfIterations, + kdfType: tokenResponse.kdf, + }, }, - }, - tokens: { - ...new AccountTokens(), - ...{ - accessToken: tokenResponse.accessToken, - refreshToken: tokenResponse.refreshToken, + keys: { + ...new AccountKeys(), + ...{ + apiKeyClientSecret: clientSecret, + }, }, - }, - }); + tokens: { + ...new AccountTokens(), + ...{ + accessToken: tokenResponse.accessToken, + refreshToken: tokenResponse.refreshToken, + }, + }, + }) + ); if (tokenResponse.twoFactorToken != null) { await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); diff --git a/common/src/services/crypto.service.ts b/common/src/services/crypto.service.ts index 05c14d93..fd9d83c4 100644 --- a/common/src/services/crypto.service.ts +++ b/common/src/services/crypto.service.ts @@ -761,13 +761,13 @@ export class CryptoService implements CryptoServiceAbstraction { // Helpers protected async storeKey(key: SymmetricCryptoKey, userId?: string) { - if ( - (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) || - (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) - ) { - await this.stateService.setCryptoMasterKeyB64(key.keyB64, { userId: userId }); + if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) { + await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId }); + } else if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) { + await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId }); } else { - await this.stateService.setCryptoMasterKeyB64(null, { userId: userId }); + await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); } } diff --git a/common/src/services/environment.service.ts b/common/src/services/environment.service.ts index d95718e0..cf435d87 100644 --- a/common/src/services/environment.service.ts +++ b/common/src/services/environment.service.ts @@ -109,18 +109,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } async setUrlsFromStorage(): Promise { - const urlsObj: any = await this.stateService.getEnvironmentUrls(); - const urls = urlsObj || { - base: null, - api: null, - identity: null, - icons: null, - notifications: null, - events: null, - webVault: null, - keyConnector: null, - }; - + const urls: any = await this.stateService.getEnvironmentUrls(); const envUrls = new EnvironmentUrls(); if (urls.base) { diff --git a/common/src/services/policy.service.ts b/common/src/services/policy.service.ts index 79af726a..dce9af6f 100644 --- a/common/src/services/policy.service.ts +++ b/common/src/services/policy.service.ts @@ -78,6 +78,15 @@ export class PolicyService implements PolicyServiceAbstraction { await this.stateService.setEncryptedPolicies(null, { userId: userId }); } + async getMasterPasswordPoliciesForInvitedUsers( + orgId: string + ): Promise { + const userId = await this.stateService.getUserId(); + const response = await this.apiService.getPoliciesByInvitedUser(orgId, userId); + const policies = await this.mapPoliciesFromToken(response); + return this.getMasterPasswordPolicyOptions(policies); + } + async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise { let enforcedOptions: MasterPasswordPolicyOptions = null; diff --git a/common/src/services/state.service.ts b/common/src/services/state.service.ts index 8fc30f8b..85280996 100644 --- a/common/src/services/state.service.ts +++ b/common/src/services/state.service.ts @@ -1,6 +1,12 @@ import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; -import { Account } from "../models/domain/account"; +import { + Account, + AccountData, + AccountKeys, + AccountProfile, + AccountTokens, +} from "../models/domain/account"; import { LogService } from "../abstractions/log.service"; import { StorageService } from "../abstractions/storage.service"; @@ -34,19 +40,21 @@ import { SendData } from "../models/data/sendData"; import { BehaviorSubject } from "rxjs"; -import { StateMigrationService } from "./stateMigration.service"; +import { StateMigrationService } from "../abstractions/stateMigration.service"; -export class StateService implements StateServiceAbstraction { - accounts = new BehaviorSubject<{ [userId: string]: Account }>({}); +export class StateService + implements StateServiceAbstraction +{ + accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({}); activeAccount = new BehaviorSubject(null); - private state: State = new State(); + protected state: State = new State(); constructor( - private storageService: StorageService, - private secureStorageService: StorageService, - private logService: LogService, - private stateMigrationService: StateMigrationService + protected storageService: StorageService, + protected secureStorageService: StorageService, + protected logService: LogService, + protected stateMigrationService: StateMigrationService ) {} async init(): Promise { @@ -60,21 +68,17 @@ export class StateService implements StateServiceAbstraction { async loadStateFromDisk() { if ((await this.getActiveUserIdFromStorage()) != null) { - const diskState = await this.storageService.get( + const diskState = await this.storageService.get>( "state", await this.defaultOnDiskOptions() ); this.state = diskState; await this.pruneInMemoryAccounts(); - await this.saveStateToStorage(this.state, await this.defaultOnDiskMemoryOptions()); await this.pushAccounts(); } } - async addAccount(account: Account) { - if (account?.profile?.userId == null) { - return; - } + async addAccount(account: TAccount) { this.state.accounts[account.profile.userId] = account; await this.scaffoldNewAccountStorage(account); await this.setActiveUser(account.profile.userId); @@ -83,7 +87,7 @@ export class StateService implements StateServiceAbstraction { async setActiveUser(userId: string): Promise { this.state.activeUserId = userId; - const storedState = await this.storageService.get( + const storedState = await this.storageService.get>( "state", await this.defaultOnDiskOptions() ); @@ -1321,47 +1325,64 @@ export class StateService implements StateServiceAbstraction { } async getEntityId(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.profile?.entityId; + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.profile?.entityId; } async setEntityId(value: string, options?: StorageOptions): Promise { const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions) + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); account.profile.entityId = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); } async getEntityType(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.profile?.entityType; + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.profile?.entityType; } async setEntityType(value: string, options?: StorageOptions): Promise { const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions) + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); account.profile.entityType = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); } async getEnvironmentUrls(options?: StorageOptions): Promise { return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.environmentUrls ?? { + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.environmentUrls ?? { + base: null, + api: null, + identity: null, + icons: null, + notifications: null, + events: null, + webVault: null, + keyConnector: null, + // TODO: this is a bug and we should use base instead for the server detail in the account switcher, otherwise self hosted urls will not show correctly server: "bitwarden.com", } ); } async setEnvironmentUrls(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( + const globals = await this.getGlobals( this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.settings.environmentUrls = value; - await this.saveAccount( - account, + globals.environmentUrls = value; + await this.saveGlobals( + globals, this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); } @@ -1402,17 +1423,20 @@ export class StateService implements StateServiceAbstraction { async getEverBeenUnlocked(options?: StorageOptions): Promise { return ( - (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile - ?.everBeenUnlocked ?? false + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.profile?.everBeenUnlocked ?? false ); } async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions) + this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); account.profile.everBeenUnlocked = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); } async getForcePasswordReset(options?: StorageOptions): Promise { @@ -1874,46 +1898,54 @@ export class StateService implements StateServiceAbstraction { } async getSsoCodeVerifier(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.profile?.ssoCodeVerifier; + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.ssoCodeVerifier; } async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions) + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.ssoCodeVerifier = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.profile.ssoCodeVerifier = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); } async getSsoOrgIdentifier(options?: StorageOptions): Promise { return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.profile?.ssoOrganizationIdentifier; + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.ssoOrganizationIdentifier; } async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( + const globals = await this.getGlobals( this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); - account.profile.ssoOrganizationIdentifier = value; - await this.saveAccount( - account, + globals.ssoOrganizationIdentifier = value; + await this.saveGlobals( + globals, this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); } async getSsoState(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.profile?.ssoState; + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.ssoState; } async setSsoState(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions) + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.ssoState = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.profile.ssoState = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); } async getTheme(options?: StorageOptions): Promise { @@ -1973,10 +2005,7 @@ export class StateService implements StateServiceAbstraction { const accountVaultTimeout = ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) )?.settings?.vaultTimeout; - const globalVaultTimeout = ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.vaultTimeout; - return accountVaultTimeout ?? globalVaultTimeout ?? 15; + return accountVaultTimeout; } async setVaultTimeout(value: number, options?: StorageOptions): Promise { @@ -2039,7 +2068,7 @@ export class StateService implements StateServiceAbstraction { ); } - private async getGlobals(options: StorageOptions): Promise { + protected async getGlobals(options: StorageOptions): Promise { let globals: GlobalState; if (this.useMemory(options.storageLocation)) { globals = this.getGlobalsFromMemory(); @@ -2052,39 +2081,41 @@ export class StateService implements StateServiceAbstraction { return globals ?? new GlobalState(); } - private async saveGlobals(globals: GlobalState, options: StorageOptions) { + protected async saveGlobals(globals: GlobalState, options: StorageOptions) { return this.useMemory(options.storageLocation) ? this.saveGlobalsToMemory(globals) : await this.saveGlobalsToDisk(globals, options); } - private getGlobalsFromMemory(): GlobalState { + protected getGlobalsFromMemory(): GlobalState { return this.state.globals; } - private async getGlobalsFromDisk(options: StorageOptions): Promise { - return (await this.storageService.get("state", options))?.globals; + protected async getGlobalsFromDisk(options: StorageOptions): Promise { + return (await this.storageService.get>("state", options))?.globals; } - private saveGlobalsToMemory(globals: GlobalState): void { + protected saveGlobalsToMemory(globals: GlobalState): void { this.state.globals = globals; } - private async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise { + protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise { if (options.useSecureStorage) { - const state = (await this.secureStorageService.get("state", options)) ?? new State(); + const state = + (await this.secureStorageService.get>("state", options)) ?? new State(); state.globals = globals; await this.secureStorageService.save("state", state, options); } else { - const state = (await this.storageService.get("state", options)) ?? new State(); + const state = + (await this.storageService.get>("state", options)) ?? new State(); state.globals = globals; await this.saveStateToStorage(state, options); } } - private async getAccount(options: StorageOptions): Promise { + protected async getAccount(options: StorageOptions): Promise { try { - let account: Account; + let account: TAccount; if (this.useMemory(options.storageLocation)) { account = this.getAccountFromMemory(options); } @@ -2093,51 +2124,51 @@ export class StateService implements StateServiceAbstraction { account = await this.getAccountFromDisk(options); } - return account != null ? new Account(account) : null; + return account; } catch (e) { this.logService.error(e); } } - private getAccountFromMemory(options: StorageOptions): Account { + protected getAccountFromMemory(options: StorageOptions): TAccount { if (this.state.accounts == null) { return null; } return this.state.accounts[this.getUserIdFromMemory(options)]; } - private getUserIdFromMemory(options: StorageOptions): string { + protected getUserIdFromMemory(options: StorageOptions): string { return options?.userId != null ? this.state.accounts[options.userId]?.profile?.userId : this.state.activeUserId; } - private async getAccountFromDisk(options: StorageOptions): Promise { + protected async getAccountFromDisk(options: StorageOptions): Promise { if (options?.userId == null && this.state.activeUserId == null) { return null; } const state = options?.useSecureStorage - ? (await this.secureStorageService.get("state", options)) ?? - (await this.storageService.get( + ? (await this.secureStorageService.get>("state", options)) ?? + (await this.storageService.get>( "state", this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }) )) - : await this.storageService.get("state", options); + : await this.storageService.get>("state", options); return state?.accounts[options?.userId ?? this.state.activeUserId]; } - private useMemory(storageLocation: StorageLocation) { + protected useMemory(storageLocation: StorageLocation) { return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both; } - private useDisk(storageLocation: StorageLocation) { + protected useDisk(storageLocation: StorageLocation) { return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both; } - private async saveAccount( - account: Account, + protected async saveAccount( + account: TAccount, options: StorageOptions = { storageLocation: StorageLocation.Both, useSecureStorage: false, @@ -2148,86 +2179,75 @@ export class StateService implements StateServiceAbstraction { : await this.saveAccountToDisk(account, options); } - private async saveAccountToDisk(account: Account, options: StorageOptions): Promise { + protected async saveAccountToDisk(account: TAccount, options: StorageOptions): Promise { const storageLocation = options.useSecureStorage ? this.secureStorageService : this.storageService; - const state = (await storageLocation.get("state", options)) ?? new State(); + const state = + (await storageLocation.get>("state", options)) ?? new State(); state.accounts[account.profile.userId] = account; await storageLocation.save("state", state, options); await this.pushAccounts(); } - private async saveAccountToMemory(account: Account): Promise { + protected async saveAccountToMemory(account: TAccount): Promise { if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) { this.state.accounts[account.profile.userId] = account; } await this.pushAccounts(); } - private async scaffoldNewAccountStorage(account: Account): Promise { + protected async scaffoldNewAccountStorage(account: TAccount): Promise { await this.scaffoldNewAccountLocalStorage(account); await this.scaffoldNewAccountSessionStorage(account); await this.scaffoldNewAccountMemoryStorage(account); } - private async scaffoldNewAccountLocalStorage(account: Account): Promise { + protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise { const storedState = - (await this.storageService.get("state", await this.defaultOnDiskLocalOptions())) ?? - new State(); + (await this.storageService.get>( + "state", + await this.defaultOnDiskLocalOptions() + )) ?? new State(); const storedAccount = storedState.accounts[account.profile.userId]; if (storedAccount != null) { - account = { - settings: storedAccount.settings, - profile: account.profile, - tokens: account.tokens, - keys: account.keys, - data: account.data, - }; + account.settings = storedAccount.settings; } storedState.accounts[account.profile.userId] = account; await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions()); } - private async scaffoldNewAccountMemoryStorage(account: Account): Promise { + protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise { const storedState = - (await this.storageService.get("state", await this.defaultOnDiskMemoryOptions())) ?? - new State(); + (await this.storageService.get>( + "state", + await this.defaultOnDiskMemoryOptions() + )) ?? new State(); const storedAccount = storedState.accounts[account.profile.userId]; if (storedAccount != null) { - account = { - settings: storedAccount.settings, - profile: account.profile, - tokens: account.tokens, - keys: account.keys, - data: account.data, - }; + account.settings = storedAccount.settings; } storedState.accounts[account.profile.userId] = account; await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions()); } - private async scaffoldNewAccountSessionStorage(account: Account): Promise { + protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise { const storedState = - (await this.storageService.get("state", await this.defaultOnDiskOptions())) ?? - new State(); + (await this.storageService.get>( + "state", + await this.defaultOnDiskOptions() + )) ?? new State(); const storedAccount = storedState.accounts[account.profile.userId]; if (storedAccount != null) { - account = { - settings: storedAccount.settings, - profile: account.profile, - tokens: account.tokens, - keys: account.keys, - data: account.data, - }; + account.settings = storedAccount.settings; } storedState.accounts[account.profile.userId] = account; await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions()); } - private async pushAccounts(): Promise { + protected async pushAccounts(): Promise { await this.pruneInMemoryAccounts(); if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) { this.accounts.next(null); @@ -2237,7 +2257,7 @@ export class StateService implements StateServiceAbstraction { this.accounts.next(this.state.accounts); } - private reconcileOptions( + protected reconcileOptions( requestedOptions: StorageOptions, defaultOptions: StorageOptions ): StorageOptions { @@ -2255,11 +2275,11 @@ export class StateService implements StateServiceAbstraction { return requestedOptions; } - private get defaultInMemoryOptions(): StorageOptions { + protected get defaultInMemoryOptions(): StorageOptions { return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId }; } - private async defaultOnDiskOptions(): Promise { + protected async defaultOnDiskOptions(): Promise { return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Session, @@ -2268,7 +2288,7 @@ export class StateService implements StateServiceAbstraction { }; } - private async defaultOnDiskLocalOptions(): Promise { + protected async defaultOnDiskLocalOptions(): Promise { return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Local, @@ -2277,7 +2297,7 @@ export class StateService implements StateServiceAbstraction { }; } - private async defaultOnDiskMemoryOptions(): Promise { + protected async defaultOnDiskMemoryOptions(): Promise { return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Memory, @@ -2286,7 +2306,7 @@ export class StateService implements StateServiceAbstraction { }; } - private async defaultSecureStorageOptions(): Promise { + protected async defaultSecureStorageOptions(): Promise { return { storageLocation: StorageLocation.Disk, useSecureStorage: true, @@ -2294,46 +2314,38 @@ export class StateService implements StateServiceAbstraction { }; } - private async getActiveUserIdFromStorage(): Promise { - const state = await this.storageService.get("state"); + protected async getActiveUserIdFromStorage(): Promise { + const state = await this.storageService.get>("state"); return state?.activeUserId; } - private async removeAccountFromLocalStorage( + protected async removeAccountFromLocalStorage( userId: string = this.state.activeUserId ): Promise { - const state = await this.storageService.get("state", { + const state = await this.storageService.get>("state", { htmlStorageLocation: HtmlStorageLocation.Local, }); if (state?.accounts[userId] == null) { return; } - - state.accounts[userId] = new Account({ - settings: state.accounts[userId].settings, - }); - + state.accounts[userId] = this.resetAccount(state.accounts[userId]); await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions()); } - private async removeAccountFromSessionStorage( + protected async removeAccountFromSessionStorage( userId: string = this.state.activeUserId ): Promise { - const state = await this.storageService.get("state", { + const state = await this.storageService.get>("state", { htmlStorageLocation: HtmlStorageLocation.Session, }); if (state?.accounts[userId] == null) { return; } - - state.accounts[userId] = new Account({ - settings: state.accounts[userId].settings, - }); - + state.accounts[userId] = this.resetAccount(state.accounts[userId]); await this.saveStateToStorage(state, await this.defaultOnDiskOptions()); } - private async removeAccountFromSecureStorage( + protected async removeAccountFromSecureStorage( userId: string = this.state.activeUserId ): Promise { await this.setCryptoMasterKeyAuto(null, { userId: userId }); @@ -2341,15 +2353,18 @@ export class StateService implements StateServiceAbstraction { await this.setCryptoMasterKeyB64(null, { userId: userId }); } - private removeAccountFromMemory(userId: string = this.state.activeUserId): void { + protected removeAccountFromMemory(userId: string = this.state.activeUserId): void { delete this.state.accounts[userId]; } - private async saveStateToStorage(state: State, options: StorageOptions): Promise { + protected async saveStateToStorage( + state: State, + options: StorageOptions + ): Promise { await this.storageService.save("state", state, options); } - private async pruneInMemoryAccounts() { + protected async pruneInMemoryAccounts() { // We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state for (const userId in this.state.accounts) { if (!(await this.getIsAuthenticated({ userId: userId }))) { @@ -2357,4 +2372,13 @@ export class StateService implements StateServiceAbstraction { } } } + + // settings persist even on reset + protected resetAccount(account: TAccount) { + account.data = new AccountData(); + account.keys = new AccountKeys(); + account.profile = new AccountProfile(); + account.tokens = new AccountTokens(); + return account; + } } diff --git a/common/src/services/stateMigration.service.ts b/common/src/services/stateMigration.service.ts index bab3667f..7f36906e 100644 --- a/common/src/services/stateMigration.service.ts +++ b/common/src/services/stateMigration.service.ts @@ -2,6 +2,7 @@ import { StorageService } from "../abstractions/storage.service"; import { Account } from "../models/domain/account"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { GlobalState } from "../models/domain/globalState"; import { State } from "../models/domain/state"; import { StorageOptions } from "../models/domain/storageOptions"; @@ -16,6 +17,7 @@ import { SendData } from "../models/data/sendData"; import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; import { KdfType } from "../enums/kdfType"; +import { StateVersion } from "../enums/stateVersion"; // Originally (before January 2022) storage was handled as a flat key/value pair store. // With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration. @@ -111,25 +113,27 @@ const v1KeyPrefixes = { }; export class StateMigrationService { - readonly latestVersion: number = 2; - constructor( - private storageService: StorageService, - private secureStorageService: StorageService + protected storageService: StorageService, + protected secureStorageService: StorageService ) {} async needsMigration(): Promise { - const currentStateVersion = (await this.storageService.get("state"))?.globals - ?.stateVersion; - return currentStateVersion == null || currentStateVersion < this.latestVersion; + const currentStateVersion = ( + await this.storageService.get>("state", { + htmlStorageLocation: HtmlStorageLocation.Local, + }) + )?.globals?.stateVersion; + return currentStateVersion == null || currentStateVersion < StateVersion.Latest; } async migrate(): Promise { let currentStateVersion = - (await this.storageService.get("state"))?.globals?.stateVersion ?? 1; - while (currentStateVersion < this.latestVersion) { + (await this.storageService.get>("state"))?.globals?.stateVersion ?? + StateVersion.One; + while (currentStateVersion < StateVersion.Latest) { switch (currentStateVersion) { - case 1: + case StateVersion.One: await this.migrateStateFrom1To2(); break; } @@ -138,15 +142,13 @@ export class StateMigrationService { } } - private async migrateStateFrom1To2(): Promise { + protected async migrateStateFrom1To2(): Promise { const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local }; const userId = await this.storageService.get("userId"); - const initialState: State = + const initialState: State = userId == null ? { - globals: { - stateVersion: 2, - }, + globals: new GlobalState(), accounts: {}, activeUserId: null, } @@ -174,6 +176,7 @@ export class StateMigrationService { v1Keys.enableBiometric, options ), + environmentUrls: await this.storageService.get(v1Keys.environmentUrls, options), installedVersion: await this.storageService.get( v1Keys.installedVersion, options @@ -192,11 +195,20 @@ export class StateMigrationService { ), openAtLogin: await this.storageService.get(v1Keys.openAtLogin, options), organizationInvitation: await this.storageService.get("", options), + ssoCodeVerifier: await this.storageService.get( + v1Keys.ssoCodeVerifier, + options + ), + ssoOrganizationIdentifier: await this.storageService.get( + v1Keys.ssoIdentifier, + options + ), + ssoState: null, rememberedEmail: await this.storageService.get( v1Keys.rememberedEmail, options ), - stateVersion: 2, + stateVersion: StateVersion.Two, theme: await this.storageService.get(v1Keys.theme, options), twoFactorToken: await this.storageService.get( v1KeyPrefixes.twoFactorToken + userId, @@ -327,15 +339,6 @@ export class StateMigrationService { keyHash: await this.storageService.get(v1Keys.keyHash, options), lastActive: await this.storageService.get(v1Keys.lastActive, options), lastSync: null, - ssoCodeVerifier: await this.storageService.get( - v1Keys.ssoCodeVerifier, - options - ), - ssoOrganizationIdentifier: await this.storageService.get( - v1Keys.ssoIdentifier, - options - ), - ssoState: null, userId: userId, usesKeyConnector: null, }, @@ -439,10 +442,6 @@ export class StateMigrationService { options ), enableTray: await this.storageService.get(v1Keys.enableTray, options), - environmentUrls: await this.storageService.get( - v1Keys.environmentUrls, - options - ), equivalentDomains: await this.storageService.get( v1Keys.equivalentDomains, options diff --git a/common/src/services/vaultTimeout.service.ts b/common/src/services/vaultTimeout.service.ts index 544e6bc2..cd810e8c 100644 --- a/common/src/services/vaultTimeout.service.ts +++ b/common/src/services/vaultTimeout.service.ts @@ -53,7 +53,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { async isLocked(userId?: string): Promise { const neverLock = (await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) && - !(await this.stateService.getEverBeenUnlocked({ userId: userId })); + (await this.stateService.getEverBeenUnlocked({ userId: userId })); if (neverLock) { // TODO: This also _sets_ the key so when we check memory in the next line it finds a key. // We should refactor here. diff --git a/node/src/cli/baseProgram.ts b/node/src/cli/baseProgram.ts index e8c15f9a..b2ddd22f 100644 --- a/node/src/cli/baseProgram.ts +++ b/node/src/cli/baseProgram.ts @@ -9,7 +9,7 @@ import { StringResponse } from "./models/response/stringResponse"; export abstract class BaseProgram { constructor( - private stateService: StateService, + protected stateService: StateService, private writeLn: (s: string, finalLine: boolean, error: boolean) => void ) {}