diff --git a/jslib-removal-plan.md b/jslib-removal-plan.md new file mode 100644 index 00000000..a9b83fa2 --- /dev/null +++ b/jslib-removal-plan.md @@ -0,0 +1,745 @@ +# Plan: Remove StateService and jslib Dependencies + +## Context + +Directory Connector currently depends on StateService from jslib, which is a massive pre-StateProvider monolith containing 200+ getter/setter methods for all Bitwarden clients. This creates significant maintenance burden and blocks deletion of unused jslib code. + +**Current State (Phase 1 Complete):** + +- ✅ StateServiceVNext has been implemented with a flat key-value structure +- ✅ Migration service handles transition from old account-based structure to new flat structure +- ⚠️ Both old and new StateService implementations coexist during migration +- ❌ Three jslib services still depend on old StateService: TokenService, CryptoService, EnvironmentService +- ❌ Two Electron components depend on old StateService: WindowMain, TrayMain + +**Problem:** +The old StateService cannot be removed until all dependencies are eliminated. Analysis reveals: + +- **TokenService**: Used for API authentication (9/32 methods actually used) +- **CryptoService**: Completely unused by DC (0/61 methods used) - carried over from monolith +- **EnvironmentService**: Used for custom server URLs (4/11 methods used) +- **WindowMain/TrayMain**: Used for Electron window/tray state persistence (6 methods total) + +**Goal:** +Replace jslib services with simplified DC-specific implementations that use StateServiceVNext, enabling complete removal of old StateService and unlocking Phase 2 (jslib code cleanup). + +**User Decisions:** + +1. ✅ Create simplified DC-specific versions of Token/Environment services (clean break from jslib) +2. ✅ Keep WindowMain/TrayMain as-is (minimize scope, focus on StateService removal) +3. ✅ Automatic migration on first launch (transparent to users) + +## Critical Files + +### Files to Create (New Implementations) + +- `src/services/token/token.service.ts` - DC-specific token service +- `src/abstractions/token.service.ts` - Token service interface +- `src/services/environment/environment.service.ts` - DC-specific environment service +- `src/abstractions/environment.service.ts` - Environment service interface +- `src/utils/jwt.util.ts` - JWT decoding utility (no dependencies) + +### Files to Modify (Update Dependencies) + +- `src/services/api.service.ts` - Switch from jslib TokenService to DC TokenService +- `src/services/auth.service.ts` - Update EnvironmentService import +- `src/services/sync.service.ts` - Update EnvironmentService import +- `src/commands/config.command.ts` - Update EnvironmentService import +- `src/bwdc.ts` - Remove old StateService, instantiate new services +- `src/main.ts` - Remove old StateService, instantiate new services +- `src/app/services/services.module.ts` - Remove old StateService, provide new services +- `src/app/app.component.ts` - Update TokenService import +- `jslib/electron/src/window.main.ts` - Adapt to use StateServiceVNext +- `jslib/electron/src/tray.main.ts` - Adapt to use StateServiceVNext + +### Files to Delete (After Migration) + +- `jslib/common/src/services/token.service.ts` - jslib TokenService +- `jslib/common/src/abstractions/token.service.ts` - jslib TokenService interface +- `jslib/common/src/services/crypto.service.ts` - Unused CryptoService +- `jslib/common/src/abstractions/crypto.service.ts` - Unused CryptoService interface +- `jslib/common/src/services/environment.service.ts` - jslib EnvironmentService +- `jslib/common/src/abstractions/environment.service.ts` - jslib EnvironmentService interface +- `src/services/state-service/state.service.ts` - Old DC StateService +- `src/abstractions/state.service.ts` - Old DC StateService interface +- `jslib/common/src/services/state.service.ts` - Old jslib StateService +- `jslib/common/src/abstractions/state.service.ts` - Old jslib StateService interface + +## Implementation Plan + +### Step 1: Create JWT Utility (No Dependencies) + +Create `src/utils/jwt.util.ts` with standalone JWT decoding function: + +```typescript +export interface DecodedToken { + exp: number; + iat: number; + nbf: number; + sub: string; // user ID + client_id?: string; + [key: string]: any; +} + +export function decodeJwt(token: string): DecodedToken { + // Validate JWT structure (3 parts: header.payload.signature) + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + + // Decode payload (base64url to JSON) + const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + + return JSON.parse(atob(payload)); +} + +export function getTokenExpirationDate(token: string): Date | null { + const decoded = decodeJwt(token); + if (!decoded.exp) return null; + return new Date(decoded.exp * 1000); +} + +export function tokenSecondsRemaining(token: string, offsetSeconds = 0): number { + const expDate = getTokenExpirationDate(token); + if (!expDate) return 0; + + const msRemaining = expDate.getTime() - Date.now() - offsetSeconds * 1000; + return Math.floor(msRemaining / 1000); +} + +export function tokenNeedsRefresh(token: string, minutesBeforeExpiration = 5): boolean { + const secondsRemaining = tokenSecondsRemaining(token); + return secondsRemaining < minutesBeforeExpiration * 60; +} +``` + +**Why:** Standalone utility avoids service dependencies, can be tested independently, reusable. + +### Step 2: Create DC TokenService + +Create `src/abstractions/token.service.ts`: + +```typescript +export interface TokenService { + // Token storage + setTokens( + accessToken: string, + refreshToken: string, + clientIdClientSecret?: [string, string], + ): Promise; + getToken(): Promise; + getRefreshToken(): Promise; + clearToken(): Promise; + + // API key authentication + getClientId(): Promise; + getClientSecret(): Promise; + + // Two-factor token (rarely used) + getTwoFactorToken(): Promise; + clearTwoFactorToken(): Promise; + + // Token validation (delegates to jwt.util) + decodeToken(token?: string): Promise; + tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise; +} +``` + +Create `src/services/token/token.service.ts`: + +```typescript +import { StateServiceVNext } from "@/abstractions/state-vNext.service"; +import { SecureStorageService } from "@/jslib/common/src/abstractions/storage.service"; +import { TokenService as ITokenService } from "@/abstractions/token.service"; +import { + decodeJwt, + tokenNeedsRefresh as checkTokenNeedsRefresh, + DecodedToken, +} from "@/utils/jwt.util"; + +export class TokenService implements ITokenService { + // Storage keys + private TOKEN_KEY = "accessToken"; + private REFRESH_TOKEN_KEY = "refreshToken"; + private CLIENT_ID_KEY = "apiKeyClientId"; + private CLIENT_SECRET_KEY = "apiKeyClientSecret"; + private TWO_FACTOR_TOKEN_KEY = "twoFactorToken"; + + constructor( + private stateService: StateServiceVNext, + private secureStorageService: SecureStorageService, + ) {} + + async setTokens( + accessToken: string, + refreshToken: string, + clientIdClientSecret?: [string, string], + ): Promise { + await this.secureStorageService.save(this.TOKEN_KEY, accessToken); + await this.secureStorageService.save(this.REFRESH_TOKEN_KEY, refreshToken); + + if (clientIdClientSecret) { + await this.secureStorageService.save(this.CLIENT_ID_KEY, clientIdClientSecret[0]); + await this.secureStorageService.save(this.CLIENT_SECRET_KEY, clientIdClientSecret[1]); + } + } + + async getToken(): Promise { + return await this.secureStorageService.get(this.TOKEN_KEY); + } + + async getRefreshToken(): Promise { + return await this.secureStorageService.get(this.REFRESH_TOKEN_KEY); + } + + async clearToken(): Promise { + await this.secureStorageService.remove(this.TOKEN_KEY); + await this.secureStorageService.remove(this.REFRESH_TOKEN_KEY); + await this.secureStorageService.remove(this.CLIENT_ID_KEY); + await this.secureStorageService.remove(this.CLIENT_SECRET_KEY); + } + + async getClientId(): Promise { + return await this.secureStorageService.get(this.CLIENT_ID_KEY); + } + + async getClientSecret(): Promise { + return await this.secureStorageService.get(this.CLIENT_SECRET_KEY); + } + + async getTwoFactorToken(): Promise { + return await this.secureStorageService.get(this.TWO_FACTOR_TOKEN_KEY); + } + + async clearTwoFactorToken(): Promise { + await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY); + } + + async decodeToken(token?: string): Promise { + const tokenToUse = token ?? (await this.getToken()); + if (!tokenToUse) return null; + + try { + return decodeJwt(tokenToUse); + } catch { + return null; + } + } + + async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise { + const token = await this.getToken(); + if (!token) return true; + + try { + return checkTokenNeedsRefresh(token, minutesBeforeExpiration); + } catch { + return true; + } + } +} +``` + +Create `src/services/token/token.service.spec.ts` with comprehensive tests covering: + +- Token storage/retrieval +- Token clearing +- JWT decoding +- Token expiration logic +- Error handling for malformed tokens + +### Step 3: Create DC EnvironmentService + +Create `src/abstractions/environment.service.ts`: + +```typescript +export interface EnvironmentUrls { + base?: string; + api?: string; + identity?: string; + webVault?: string; + icons?: string; + notifications?: string; + events?: string; + keyConnector?: string; +} + +export interface EnvironmentService { + setUrls(urls: EnvironmentUrls): Promise; + setUrlsFromStorage(): Promise; + + hasBaseUrl(): boolean; + getApiUrl(): string; + getIdentityUrl(): string; + getWebVaultUrl(): string; + getIconsUrl(): string; + getNotificationsUrl(): string; + getEventsUrl(): string; + getKeyConnectorUrl(): string; +} +``` + +Create `src/services/environment/environment.service.ts`: + +```typescript +import { StateServiceVNext } from "@/abstractions/state-vNext.service"; +import { + EnvironmentService as IEnvironmentService, + EnvironmentUrls, +} from "@/abstractions/environment.service"; + +export class EnvironmentService implements IEnvironmentService { + private readonly DEFAULT_URLS = { + api: "https://api.bitwarden.com", + identity: "https://identity.bitwarden.com", + webVault: "https://vault.bitwarden.com", + icons: "https://icons.bitwarden.net", + notifications: "https://notifications.bitwarden.com", + events: "https://events.bitwarden.com", + }; + + private urls: EnvironmentUrls = {}; + + constructor(private stateService: StateServiceVNext) {} + + async setUrls(urls: EnvironmentUrls): Promise { + // Normalize URLs: trim whitespace, remove trailing slashes, add https:// if missing + const normalized: EnvironmentUrls = {}; + + for (const [key, value] of Object.entries(urls)) { + if (!value) continue; + + let url = value.trim(); + url = url.replace(/\/+$/, ""); // Remove trailing slashes + + if (!/^https?:\/\//i.test(url)) { + url = `https://${url}`; + } + + normalized[key] = url; + } + + this.urls = normalized; + await this.stateService.setEnvironmentUrls(normalized); + } + + async setUrlsFromStorage(): Promise { + const stored = await this.stateService.getEnvironmentUrls(); + this.urls = stored ?? {}; + } + + hasBaseUrl(): boolean { + return !!this.urls.base; + } + + getApiUrl(): string { + return this.urls.api ?? this.urls.base + "/api" ?? this.DEFAULT_URLS.api; + } + + getIdentityUrl(): string { + return this.urls.identity ?? this.urls.base + "/identity" ?? this.DEFAULT_URLS.identity; + } + + getWebVaultUrl(): string { + return this.urls.webVault ?? this.urls.base ?? this.DEFAULT_URLS.webVault; + } + + getIconsUrl(): string { + return this.urls.icons ?? this.urls.base + "/icons" ?? this.DEFAULT_URLS.icons; + } + + getNotificationsUrl(): string { + return ( + this.urls.notifications ?? + this.urls.base + "/notifications" ?? + this.DEFAULT_URLS.notifications + ); + } + + getEventsUrl(): string { + return this.urls.events ?? this.urls.base + "/events" ?? this.DEFAULT_URLS.events; + } + + getKeyConnectorUrl(): string { + return this.urls.keyConnector ?? ""; + } +} +``` + +Create `src/services/environment/environment.service.spec.ts` with tests covering: + +- URL normalization (trailing slashes, https prefix) +- Storage persistence +- Default URL fallbacks +- Custom URL override +- Base URL derivation + +### Step 4: Add Environment URL Storage to StateServiceVNext + +Update `src/models/state.model.ts` to add environment URL storage key: + +```typescript +export const StorageKeysVNext = { + // ... existing keys ... + environmentUrls: "environmentUrls", +}; +``` + +Update `src/abstractions/state-vNext.service.ts` to add methods: + +```typescript +export interface StateServiceVNext { + // ... existing methods ... + getEnvironmentUrls(): Promise; + setEnvironmentUrls(urls: EnvironmentUrls): Promise; +} +``` + +Update `src/services/state-service/state-vNext.service.ts` implementation to add storage methods. + +### Step 5: Update StateMigrationService for Token/Environment Data + +Update `src/services/state-service/stateMigration.service.ts` to migrate: + +**Token data (from secure storage):** + +- `accessToken` → `accessToken` (same key, no change needed) +- `refreshToken` → `refreshToken` (same key, no change needed) +- `apiKeyClientId` → `apiKeyClientId` (same key, no change needed) +- `apiKeyClientSecret` → `apiKeyClientSecret` (same key, no change needed) + +**Environment URLs (from account state):** + +- `environmentUrls` from account → `environmentUrls` in flat structure + +Add migration test cases to `src/services/state-service/stateMigration.service.spec.ts`. + +### Step 6: Remove CryptoService Dependencies + +Since CryptoService is completely unused by DC: + +1. Search for all imports of `CryptoService` in `src/` code +2. Remove all instantiations and injections +3. Verify no methods are actually called +4. Remove from DI containers (services.module.ts, bwdc.ts, main.ts) + +Expected: Zero usage, straightforward removal. + +### Step 7: Update WindowMain/TrayMain to Use StateServiceVNext + +Update `jslib/electron/src/window.main.ts`: + +```typescript +// Change constructor to accept StateServiceVNext instead of StateService +constructor( + private stateService: StateServiceVNext, // Changed from StateService + // ... other params +) {} + +// Update method calls to use StateServiceVNext interface +async getWindowSettings(): Promise { + return await this.stateService.getWindowSettings(); +} + +async setWindowSettings(settings: any): Promise { + await this.stateService.setWindowSettings(settings); +} +``` + +Update `jslib/electron/src/tray.main.ts` similarly: + +```typescript +constructor( + private stateService: StateServiceVNext, // Changed from StateService + // ... other params +) {} + +// Update method calls +async getEnableTray(): Promise { + return await this.stateService.getEnableTray(); +} +// ... etc for other tray settings +``` + +**Required:** Add window/tray setting storage to StateServiceVNext: + +- `getWindowSettings()` / `setWindowSettings()` +- `getEnableTray()` / `getEnableMinimizeToTray()` / `getEnableCloseToTray()` / `getAlwaysShowDock()` + +### Step 8: Update Service Registrations + +**In `src/app/services/services.module.ts`:** + +```typescript +// Remove old services +- import { StateService } from '@/services/state-service/state.service'; +- import { TokenService } from '@/jslib/common/src/services/token.service'; +- import { CryptoService } from '@/jslib/common/src/services/crypto.service'; +- import { EnvironmentService } from '@/jslib/common/src/services/environment.service'; + +// Add new services ++ import { TokenService } from '@/services/token/token.service'; ++ import { EnvironmentService } from '@/services/environment/environment.service'; + +providers: [ + // Remove old StateService provider + - { provide: StateService, useClass: StateService }, + + // Add new service providers + + { provide: TokenService, useClass: TokenService }, + + { provide: EnvironmentService, useClass: EnvironmentService }, + + // Keep StateServiceVNext + { provide: StateServiceVNext, useClass: StateServiceVNextImplementation }, +] +``` + +**In `src/bwdc.ts` (CLI):** + +```typescript +// Remove old service instantiations +- this.stateService = new StateService(/* ... */); +- this.cryptoService = new CryptoService(/* ... */); +- this.tokenService = new TokenService(/* ... */); +- this.environmentService = new EnvironmentService(this.stateService); + +// Add new service instantiations ++ this.tokenService = new TokenService(this.stateServiceVNext, secureStorageService); ++ this.environmentService = new EnvironmentService(this.stateServiceVNext); +``` + +**In `src/main.ts` (Electron):** + +```typescript +// Remove old service instantiations +- this.stateService = new StateService(/* ... */); +- this.cryptoService = new CryptoService(/* ... */); +- this.tokenService = new TokenService(/* ... */); +- this.environmentService = new EnvironmentService(this.stateService); + +// Add new service instantiations ++ this.tokenService = new TokenService(this.stateServiceVNext, secureStorageService); ++ this.environmentService = new EnvironmentService(this.stateServiceVNext); + +// Update WindowMain/TrayMain to use StateServiceVNext +- this.windowMain = new WindowMain(this.stateService, /* ... */); +- this.trayMain = new TrayMain(this.stateService, /* ... */); ++ this.windowMain = new WindowMain(this.stateServiceVNext, /* ... */); ++ this.trayMain = new TrayMain(this.stateServiceVNext, /* ... */); +``` + +### Step 9: Update Import Statements + +Update all files that import Token/Environment services: + +**Files to update:** + +- `src/services/api.service.ts` - Change TokenService import to DC version +- `src/services/auth.service.ts` - Change EnvironmentService import to DC version +- `src/services/sync.service.ts` - Change EnvironmentService import to DC version +- `src/commands/config.command.ts` - Change EnvironmentService import to DC version +- `src/app/app.component.ts` - Change TokenService import to DC version + +**Pattern:** + +```typescript +// Before +import { TokenService } from "@/jslib/common/src/services/token.service"; +import { EnvironmentService } from "@/jslib/common/src/services/environment.service"; + +// After +import { TokenService } from "@/abstractions/token.service"; +import { EnvironmentService } from "@/abstractions/environment.service"; +``` + +### Step 10: Delete Old StateService and jslib Services + +**Delete these files (after all references removed):** + +```bash +# Old StateService implementations +src/services/state-service/state.service.ts +src/abstractions/state.service.ts +jslib/common/src/services/state.service.ts +jslib/common/src/abstractions/state.service.ts + +# jslib Token/Crypto/Environment services +jslib/common/src/services/token.service.ts +jslib/common/src/abstractions/token.service.ts +jslib/common/src/services/crypto.service.ts +jslib/common/src/abstractions/crypto.service.ts +jslib/common/src/services/environment.service.ts +jslib/common/src/abstractions/environment.service.ts +``` + +**Rename StateServiceVNext to StateService:** + +```bash +# Rename files +mv src/services/state-service/state-vNext.service.ts src/services/state-service/state.service.ts +mv src/services/state-service/state-vNext.service.spec.ts src/services/state-service/state.service.spec.ts +mv src/abstractions/state-vNext.service.ts src/abstractions/state.service.ts + +# Update all imports from StateServiceVNext to StateService +# Find and replace: StateServiceVNext → StateService +``` + +### Step 11: Update Tests + +**Update existing tests that mock StateService:** + +- Update mocks to use new StateService interface (flat key-value structure) +- Remove mocks for Token/Crypto/Environment services where they inject old versions +- Add mocks for new DC Token/Environment services + +**Add new test files:** + +- `src/services/token/token.service.spec.ts` (created in Step 2) +- `src/services/environment/environment.service.spec.ts` (created in Step 3) +- `src/utils/jwt.util.spec.ts` (JWT utility tests) + +**Update integration tests:** + +- Verify token storage/retrieval works correctly +- Verify environment URL configuration persists +- Verify window/tray settings persist in Electron app + +## Verification Plan + +### Unit Tests + +```bash +npm test # Run all unit tests +``` + +**Expected:** + +- All new service tests pass (TokenService, EnvironmentService, JWT util) +- All existing tests pass with updated mocks +- No test failures due to StateService removal + +### Integration Tests + +```bash +npm run test:integration +``` + +**Expected:** + +- LDAP sync tests pass +- Authentication flow works correctly +- Configuration persistence works + +### Manual Testing - CLI + +```bash +# Build and run CLI +npm run build:cli:watch +node ./build-cli/bwdc.js --help + +# Test authentication +node ./build-cli/bwdc.js config server https://vault.bitwarden.com +node ./build-cli/bwdc.js login --apikey + +# Test sync +node ./build-cli/bwdc.js config directory ldap +node ./build-cli/bwdc.js config ldap.hostname ldap.example.com +node ./build-cli/bwdc.js sync + +# Test logout +node ./build-cli/bwdc.js logout +``` + +**Verify:** + +- ✅ Server URL configuration persists +- ✅ Login stores tokens correctly +- ✅ Token refresh works automatically +- ✅ Sync completes successfully +- ✅ Logout clears tokens + +### Manual Testing - Desktop App + +```bash +# Build and run desktop app +npm run electron +``` + +**Verify:** + +- ✅ Window position/size persists across restarts +- ✅ "Always on top" setting persists +- ✅ Tray icon shows/hides based on settings +- ✅ Minimize/close to tray works +- ✅ Login/logout flow works +- ✅ Sync functionality works +- ✅ Custom server URL configuration works + +### Migration Testing + +**Test migration from existing installation:** + +1. Install current production version +2. Configure directory connection and run sync +3. Install new version with StateService removal +4. Launch app - verify automatic migration occurs +5. Verify all settings preserved: + - Directory configuration + - Organization ID + - Server URLs + - Window/tray settings + - Authentication tokens + +**Expected:** + +- ✅ Migration runs automatically on first launch +- ✅ All user data preserved +- ✅ No user action required +- ✅ App functions identically to before + +### Regression Testing + +Run through all major workflows: + +1. **Configuration**: Set up each directory type (LDAP, Entra, Google, Okta, OneLogin) +2. **Authentication**: Login with API key, verify token refresh +3. **Sync**: Full sync, incremental sync (delta tokens), detect changes via hash +4. **Custom server**: Configure self-hosted Bitwarden server +5. **Electron features**: Window management, tray behavior + +**Expected:** No regressions in functionality. + +## Rollback Plan + +If critical issues discovered post-deployment: + +1. **Revert commit** removing StateService +2. **Keep StateServiceVNext in parallel** (already coexisting) +3. **Debug issues** in development +4. **Re-attempt removal** after fixes + +**Risk Assessment:** Low - StateServiceVNext has been in production since Phase 1 PR merge, proven stable. + +## Success Criteria + +- ✅ All old StateService implementations deleted +- ✅ StateServiceVNext renamed to StateService (becomes primary) +- ✅ jslib TokenService, CryptoService, EnvironmentService deleted +- ✅ DC-specific Token/Environment services implemented and tested +- ✅ All unit tests pass +- ✅ All integration tests pass +- ✅ Manual testing shows no regressions +- ✅ Migration from old state structure works automatically +- ✅ WindowMain/TrayMain adapted to new StateService +- ✅ Zero references to old StateService in codebase + +## Next Steps (After Completion) + +This unblocks **Phase 2: Remove Remaining jslib Code**: + +- Delete unused jslib models (AccountData, AccountSettings, etc.) +- Delete unused jslib services that referenced StateService +- Clean up jslib/common folder of unused client code +- Potentially merge remaining jslib code into src/ (flatten structure) + +Estimated effort: 2-3 days for experienced developer familiar with codebase. diff --git a/jslib/angular/src/components/environment.component.ts b/jslib/angular/src/components/environment.component.ts deleted file mode 100644 index 96d77205..00000000 --- a/jslib/angular/src/components/environment.component.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Directive, EventEmitter, Output } from "@angular/core"; - -import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service"; -import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; -import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service"; - -@Directive() -export class EnvironmentComponent { - @Output() onSaved = new EventEmitter(); - - iconsUrl: string; - identityUrl: string; - apiUrl: string; - webVaultUrl: string; - notificationsUrl: string; - baseUrl: string; - showCustom = false; - - constructor( - protected platformUtilsService: PlatformUtilsService, - protected environmentService: EnvironmentService, - protected i18nService: I18nService, - ) { - const urls = this.environmentService.getUrls(); - - this.baseUrl = urls.base || ""; - this.webVaultUrl = urls.webVault || ""; - this.apiUrl = urls.api || ""; - this.identityUrl = urls.identity || ""; - this.iconsUrl = urls.icons || ""; - this.notificationsUrl = urls.notifications || ""; - } - - async submit() { - const resUrls = await this.environmentService.setUrls({ - base: this.baseUrl, - api: this.apiUrl, - identity: this.identityUrl, - webVault: this.webVaultUrl, - icons: this.iconsUrl, - notifications: this.notificationsUrl, - }); - - // re-set urls since service can change them, ex: prefixing https:// - this.baseUrl = resUrls.base; - this.apiUrl = resUrls.api; - this.identityUrl = resUrls.identity; - this.webVaultUrl = resUrls.webVault; - this.iconsUrl = resUrls.icons; - this.notificationsUrl = resUrls.notifications; - - this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved")); - this.saved(); - } - - toggleCustom() { - this.showCustom = !this.showCustom; - } - - protected saved() { - this.onSaved.emit(); - } -} diff --git a/jslib/angular/src/services/jslib-services.module.ts b/jslib/angular/src/services/jslib-services.module.ts deleted file mode 100644 index 20331a65..00000000 --- a/jslib/angular/src/services/jslib-services.module.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { LOCALE_ID, NgModule } from "@angular/core"; - -import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service"; -import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service"; -import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service"; -import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service"; -import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service"; -import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service"; -import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service"; -import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service"; -import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service"; -import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service"; -import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service"; -import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service"; -import { StateFactory } from "@/jslib/common/src/factories/stateFactory"; -import { Account } from "@/jslib/common/src/models/domain/account"; -import { GlobalState } from "@/jslib/common/src/models/domain/globalState"; -import { ApiService } from "@/jslib/common/src/services/api.service"; -import { AppIdService } from "@/jslib/common/src/services/appId.service"; -import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service"; -import { CryptoService } from "@/jslib/common/src/services/crypto.service"; -import { EnvironmentService } from "@/jslib/common/src/services/environment.service"; -import { StateService } from "@/jslib/common/src/services/state.service"; -import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service"; -import { TokenService } from "@/jslib/common/src/services/token.service"; - -import { - SafeInjectionToken, - SECURE_STORAGE, - WINDOW, -} from "../../../../src/app/services/injection-tokens"; -import { SafeProvider, safeProvider } from "../../../../src/app/services/safe-provider"; - -import { BroadcasterService } from "./broadcaster.service"; -import { ModalService } from "./modal.service"; -import { ValidationService } from "./validation.service"; - -@NgModule({ - declarations: [], - providers: [ - safeProvider({ provide: WINDOW, useValue: window }), - safeProvider({ - provide: LOCALE_ID as SafeInjectionToken, - useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale, - deps: [I18nServiceAbstraction], - }), - safeProvider(ValidationService), - safeProvider(ModalService), - safeProvider({ - provide: AppIdServiceAbstraction, - useClass: AppIdService, - deps: [StorageServiceAbstraction], - }), - safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }), - safeProvider({ - provide: EnvironmentServiceAbstraction, - useClass: EnvironmentService, - deps: [StateServiceAbstraction], - }), - safeProvider({ - provide: TokenServiceAbstraction, - useClass: TokenService, - deps: [StateServiceAbstraction], - }), - safeProvider({ - provide: CryptoServiceAbstraction, - useClass: CryptoService, - deps: [ - CryptoFunctionServiceAbstraction, - PlatformUtilsServiceAbstraction, - LogService, - StateServiceAbstraction, - ], - }), - safeProvider({ - provide: ApiServiceAbstraction, - useFactory: ( - tokenService: TokenServiceAbstraction, - platformUtilsService: PlatformUtilsServiceAbstraction, - environmentService: EnvironmentServiceAbstraction, - messagingService: MessagingServiceAbstraction, - appIdService: AppIdServiceAbstraction, - ) => - new ApiService( - tokenService, - platformUtilsService, - environmentService, - appIdService, - async (expired: boolean) => messagingService.send("logout", { expired: expired }), - ), - deps: [ - TokenServiceAbstraction, - PlatformUtilsServiceAbstraction, - EnvironmentServiceAbstraction, - MessagingServiceAbstraction, - AppIdServiceAbstraction, - ], - }), - safeProvider({ - provide: BroadcasterServiceAbstraction, - useClass: BroadcasterService, - useAngularDecorators: true, - }), - safeProvider({ - provide: StateServiceAbstraction, - useFactory: ( - storageService: StorageServiceAbstraction, - secureStorageService: StorageServiceAbstraction, - logService: LogService, - stateMigrationService: StateMigrationServiceAbstraction, - ) => - new StateService( - storageService, - secureStorageService, - logService, - stateMigrationService, - new StateFactory(GlobalState, Account), - ), - deps: [ - StorageServiceAbstraction, - SECURE_STORAGE, - LogService, - StateMigrationServiceAbstraction, - ], - }), - safeProvider({ - provide: StateMigrationServiceAbstraction, - useFactory: ( - storageService: StorageServiceAbstraction, - secureStorageService: StorageServiceAbstraction, - ) => - new StateMigrationService( - storageService, - secureStorageService, - new StateFactory(GlobalState, Account), - ), - deps: [StorageServiceAbstraction, SECURE_STORAGE], - }), - ] satisfies SafeProvider[], -}) -export class JslibServicesModule {} diff --git a/jslib/common/spec/domain/encString.spec.ts b/jslib/common/spec/domain/encString.spec.ts deleted file mode 100644 index 3932980c..00000000 --- a/jslib/common/spec/domain/encString.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Substitute, Arg } from "@fluffy-spoon/substitute"; - -import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service"; -import { EncryptionType } from "@/jslib/common/src/enums/encryptionType"; -import { EncString } from "@/jslib/common/src/models/domain/encString"; -import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey"; -import { ContainerService } from "@/jslib/common/src/services/container.service"; - -describe("EncString", () => { - afterEach(() => { - (window as any).bitwardenContainerService = undefined; - }); - - describe("Rsa2048_OaepSha256_B64", () => { - it("constructor", () => { - const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data"); - - expect(encString).toEqual({ - data: "data", - encryptedString: "3.data", - encryptionType: 3, - }); - }); - - describe("parse existing", () => { - it("valid", () => { - const encString = new EncString("3.data"); - - expect(encString).toEqual({ - data: "data", - encryptedString: "3.data", - encryptionType: 3, - }); - }); - - it("invalid", () => { - const encString = new EncString("3.data|test"); - - expect(encString).toEqual({ - encryptedString: "3.data|test", - encryptionType: 3, - }); - }); - }); - - describe("decrypt", () => { - const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data"); - - const cryptoService = Substitute.for(); - cryptoService.getOrgKey(null).resolves(null); - cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted"); - - beforeEach(() => { - (window as any).bitwardenContainerService = new ContainerService(cryptoService); - }); - - it("decrypts correctly", async () => { - const decrypted = await encString.decrypt(null); - - expect(decrypted).toBe("decrypted"); - }); - - it("result should be cached", async () => { - const decrypted = await encString.decrypt(null); - cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any()); - - expect(decrypted).toBe("decrypted"); - }); - }); - }); - - describe("AesCbc256_B64", () => { - it("constructor", () => { - const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv"); - - expect(encString).toEqual({ - data: "data", - encryptedString: "0.iv|data", - encryptionType: 0, - iv: "iv", - }); - }); - - describe("parse existing", () => { - it("valid", () => { - const encString = new EncString("0.iv|data"); - - expect(encString).toEqual({ - data: "data", - encryptedString: "0.iv|data", - encryptionType: 0, - iv: "iv", - }); - }); - - it("invalid", () => { - const encString = new EncString("0.iv|data|mac"); - - expect(encString).toEqual({ - encryptedString: "0.iv|data|mac", - encryptionType: 0, - }); - }); - }); - }); - - describe("AesCbc256_HmacSha256_B64", () => { - it("constructor", () => { - const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac"); - - expect(encString).toEqual({ - data: "data", - encryptedString: "2.iv|data|mac", - encryptionType: 2, - iv: "iv", - mac: "mac", - }); - }); - - it("valid", () => { - const encString = new EncString("2.iv|data|mac"); - - expect(encString).toEqual({ - data: "data", - encryptedString: "2.iv|data|mac", - encryptionType: 2, - iv: "iv", - mac: "mac", - }); - }); - - it("invalid", () => { - const encString = new EncString("2.iv|data"); - - expect(encString).toEqual({ - encryptedString: "2.iv|data", - encryptionType: 2, - }); - }); - }); - - it("Exit early if null", () => { - const encString = new EncString(null); - - expect(encString).toEqual({ - encryptedString: null, - }); - }); - - describe("decrypt", () => { - it("throws exception when bitwarden container not initialized", async () => { - const encString = new EncString(null); - - expect.assertions(1); - try { - await encString.decrypt(null); - } catch (e) { - expect(e.message).toEqual("global bitwardenContainerService not initialized."); - } - }); - - it("handles value it can't decrypt", async () => { - const encString = new EncString(null); - - const cryptoService = Substitute.for(); - cryptoService.getOrgKey(null).resolves(null); - cryptoService.decryptToUtf8(encString, Arg.any()).throws("error"); - - (window as any).bitwardenContainerService = new ContainerService(cryptoService); - - const decrypted = await encString.decrypt(null); - - expect(decrypted).toBe("[error: cannot decrypt]"); - - expect(encString).toEqual({ - decryptedValue: "[error: cannot decrypt]", - encryptedString: null, - }); - }); - - it("passes along key", async () => { - const encString = new EncString(null); - const key = Substitute.for(); - - const cryptoService = Substitute.for(); - cryptoService.getOrgKey(null).resolves(null); - - (window as any).bitwardenContainerService = new ContainerService(cryptoService); - - await encString.decrypt(null, key); - - cryptoService.received().decryptToUtf8(encString, key); - }); - }); -}); diff --git a/jslib/common/spec/services/stateMigration.service.ts b/jslib/common/spec/services/stateMigration.service.ts deleted file mode 100644 index 11d0ec45..00000000 --- a/jslib/common/spec/services/stateMigration.service.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; - -import { StorageService } from "@/jslib/common/src/abstractions/storage.service"; -import { StateVersion } from "@/jslib/common/src/enums/stateVersion"; -import { StateFactory } from "@/jslib/common/src/factories/stateFactory"; -import { Account } from "@/jslib/common/src/models/domain/account"; -import { GlobalState } from "@/jslib/common/src/models/domain/globalState"; -import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service"; - -const userId = "USER_ID"; - -describe("State Migration Service", () => { - let storageService: SubstituteOf; - let secureStorageService: SubstituteOf; - let stateFactory: SubstituteOf; - - let stateMigrationService: StateMigrationService; - - beforeEach(() => { - storageService = Substitute.for(); - secureStorageService = Substitute.for(); - stateFactory = Substitute.for(); - - stateMigrationService = new StateMigrationService( - storageService, - secureStorageService, - stateFactory, - ); - }); - - describe("StateVersion 3 to 4 migration", async () => { - beforeEach(() => { - const globalVersion3: Partial = { - stateVersion: StateVersion.Three, - }; - - storageService.get("global", Arg.any()).resolves(globalVersion3); - storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]); - }); - - it("clears everBeenUnlocked", async () => { - const accountVersion3: Account = { - profile: { - apiKeyClientId: null, - convertAccountToKeyConnector: null, - email: "EMAIL", - emailVerified: true, - everBeenUnlocked: true, - hasPremiumPersonally: false, - kdfIterations: 100000, - kdfType: 0, - keyHash: "KEY_HASH", - lastSync: "LAST_SYNC", - userId: userId, - usesKeyConnector: false, - forcePasswordReset: false, - }, - }; - - const expectedAccountVersion4: Account = { - profile: { - ...accountVersion3.profile, - }, - }; - delete expectedAccountVersion4.profile.everBeenUnlocked; - - storageService.get(userId, Arg.any()).resolves(accountVersion3); - - await stateMigrationService.migrate(); - - storageService.received(1).save(userId, expectedAccountVersion4, Arg.any()); - }); - - it("updates StateVersion number", async () => { - await stateMigrationService.migrate(); - - storageService.received(1).save( - "global", - Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four), - Arg.any(), - ); - }); - }); -}); diff --git a/jslib/common/spec/utils.ts b/jslib/common/spec/utils.ts index f38c0063..bf351c9a 100644 --- a/jslib/common/spec/utils.ts +++ b/jslib/common/spec/utils.ts @@ -1,7 +1,3 @@ -import { Substitute, Arg } from "@fluffy-spoon/substitute"; - -import { EncString } from "@/jslib/common/src/models/domain/encString"; - function newGuid() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; @@ -21,13 +17,6 @@ export function BuildTestObject( return Object.assign(constructor === null ? {} : new constructor(), def) as T; } -export function mockEnc(s: string): EncString { - const mock = Substitute.for(); - mock.decrypt(Arg.any(), Arg.any()).resolves(s); - - return mock; -} - export function makeStaticByteArray(length: number, start = 0) { const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { diff --git a/jslib/common/src/abstractions/crypto.service.ts b/jslib/common/src/abstractions/crypto.service.ts deleted file mode 100644 index 2cd0cd9a..00000000 --- a/jslib/common/src/abstractions/crypto.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { HashPurpose } from "../enums/hashPurpose"; -import { KdfType } from "../enums/kdfType"; -import { KeySuffixOptions } from "../enums/keySuffixOptions"; -import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; -import { EncString } from "../models/domain/encString"; -import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse"; -import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse"; -import { ProfileProviderResponse } from "../models/response/profileProviderResponse"; - -export abstract class CryptoService { - setKey: (key: SymmetricCryptoKey) => Promise; - setKeyHash: (keyHash: string) => Promise; - setEncKey: (encKey: string) => Promise; - setEncPrivateKey: (encPrivateKey: string) => Promise; - setOrgKeys: ( - orgs: ProfileOrganizationResponse[], - providerOrgs: ProfileProviderOrganizationResponse[], - ) => Promise; - setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise; - getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise; - getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise; - getKeyHash: () => Promise; - compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise; - getEncKey: (key?: SymmetricCryptoKey) => Promise; - getPublicKey: () => Promise; - getPrivateKey: () => Promise; - getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise; - getOrgKeys: () => Promise>; - getOrgKey: (orgId: string) => Promise; - getProviderKey: (providerId: string) => Promise; - hasKey: () => Promise; - hasKeyInMemory: (userId?: string) => Promise; - hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise; - hasEncKey: () => Promise; - clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise; - clearKeyHash: () => Promise; - clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise; - clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise; - clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise; - clearProviderKeys: (memoryOnly?: boolean) => Promise; - clearPinProtectedKey: () => Promise; - clearKeys: (userId?: string) => Promise; - toggleKey: () => Promise; - makeKey: ( - password: string, - salt: string, - kdf: KdfType, - kdfIterations: number, - ) => Promise; - makeKeyFromPin: ( - pin: string, - salt: string, - kdf: KdfType, - kdfIterations: number, - protectedKeyCs?: EncString, - ) => Promise; - makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>; - makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>; - makePinKey: ( - pin: string, - salt: string, - kdf: KdfType, - kdfIterations: number, - ) => Promise; - makeSendKey: (keyMaterial: ArrayBuffer) => Promise; - hashPassword: ( - password: string, - key: SymmetricCryptoKey, - hashPurpose?: HashPurpose, - ) => Promise; - makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>; - remakeEncKey: ( - key: SymmetricCryptoKey, - encKey?: SymmetricCryptoKey, - ) => Promise<[SymmetricCryptoKey, EncString]>; - encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise; - rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise; - decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise; - decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise; - decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; - randomNumber: (min: number, max: number) => Promise; - validateKey: (key: SymmetricCryptoKey) => Promise; -} diff --git a/jslib/common/src/abstractions/environment.service.ts b/jslib/common/src/abstractions/environment.service.ts index 8398b4c6..6d2a8b4f 100644 --- a/jslib/common/src/abstractions/environment.service.ts +++ b/jslib/common/src/abstractions/environment.service.ts @@ -1,34 +1,2 @@ -import { Observable } from "rxjs"; - -export type Urls = { - base?: string; - webVault?: string; - api?: string; - identity?: string; - icons?: string; - notifications?: string; - events?: string; - keyConnector?: string; -}; - -export type PayPalConfig = { - businessId?: string; - buttonAction?: string; -}; - -export abstract class EnvironmentService { - urls: Observable; - - hasBaseUrl: () => boolean; - getNotificationsUrl: () => string; - getWebVaultUrl: () => string; - getSendUrl: () => string; - getIconsUrl: () => string; - getApiUrl: () => string; - getIdentityUrl: () => string; - getEventsUrl: () => string; - getKeyConnectorUrl: () => string; - setUrlsFromStorage: () => Promise; - setUrls: (urls: Urls) => Promise; - getUrls: () => Urls; -} +// Stub file - re-exports DC EnvironmentService +export { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service"; diff --git a/jslib/common/src/abstractions/state.service.ts b/jslib/common/src/abstractions/state.service.ts index df30f264..5828bb97 100644 --- a/jslib/common/src/abstractions/state.service.ts +++ b/jslib/common/src/abstractions/state.service.ts @@ -1,218 +1,2 @@ -import { Observable } from "rxjs"; - -import { KdfType } from "../enums/kdfType"; -import { ThemeType } from "../enums/themeType"; -import { UriMatchType } from "../enums/uriMatchType"; -import { OrganizationData } from "../models/data/organizationData"; -import { ProviderData } from "../models/data/providerData"; -import { Account } from "../models/domain/account"; -import { EncString } from "../models/domain/encString"; -import { EnvironmentUrls } from "../models/domain/environmentUrls"; -import { StorageOptions } from "../models/domain/storageOptions"; -import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { WindowState } from "../models/domain/windowState"; - -export abstract class StateService { - accounts$: Observable<{ [userId: string]: T }>; - activeAccount$: Observable; - - addAccount: (account: T) => Promise; - setActiveUser: (userId: string) => Promise; - clean: (options?: StorageOptions) => Promise; - init: () => Promise; - - getAccessToken: (options?: StorageOptions) => Promise; - setAccessToken: (value: string, options?: StorageOptions) => Promise; - getAddEditCipherInfo: (options?: StorageOptions) => Promise; - setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise; - getAlwaysShowDock: (options?: StorageOptions) => Promise; - setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; - getApiKeyClientId: (options?: StorageOptions) => Promise; - setApiKeyClientId: (value: string, options?: StorageOptions) => Promise; - getApiKeyClientSecret: (options?: StorageOptions) => Promise; - setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise; - getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; - setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; - getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise; - setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise; - getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; - setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; - getBiometricLocked: (options?: StorageOptions) => Promise; - setBiometricLocked: (value: boolean, options?: StorageOptions) => Promise; - getBiometricText: (options?: StorageOptions) => Promise; - setBiometricText: (value: string, options?: StorageOptions) => Promise; - getBiometricUnlock: (options?: StorageOptions) => Promise; - setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise; - getCanAccessPremium: (options?: StorageOptions) => Promise; - getClearClipboard: (options?: StorageOptions) => Promise; - setClearClipboard: (value: number, options?: StorageOptions) => Promise; - getCollapsedGroupings: (options?: StorageOptions) => Promise; - setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise; - getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; - setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise; - getCryptoMasterKey: (options?: StorageOptions) => Promise; - setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; - getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise; - setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise; - getCryptoMasterKeyB64: (options?: StorageOptions) => Promise; - setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise; - getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; - hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; - setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise; - getDecodedToken: (options?: StorageOptions) => Promise; - setDecodedToken: (value: any, options?: StorageOptions) => Promise; - getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; - setDecryptedCryptoSymmetricKey: ( - value: SymmetricCryptoKey, - options?: StorageOptions, - ) => Promise; - getDecryptedOrganizationKeys: ( - options?: StorageOptions, - ) => Promise>; - setDecryptedOrganizationKeys: ( - value: Map, - options?: StorageOptions, - ) => Promise; - getDecryptedPinProtected: (options?: StorageOptions) => Promise; - setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise; - getDecryptedPrivateKey: (options?: StorageOptions) => Promise; - setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise; - getDecryptedProviderKeys: (options?: StorageOptions) => Promise>; - setDecryptedProviderKeys: ( - value: Map, - options?: StorageOptions, - ) => Promise; - getDefaultUriMatch: (options?: StorageOptions) => Promise; - setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise; - getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise; - setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise; - getDisableAutoTotpCopy: (options?: StorageOptions) => Promise; - setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise; - getDisableBadgeCounter: (options?: StorageOptions) => Promise; - setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise; - getDisableContextMenuItem: (options?: StorageOptions) => Promise; - setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise; - getDisableGa: (options?: StorageOptions) => Promise; - setDisableGa: (value: boolean, options?: StorageOptions) => Promise; - getEmail: (options?: StorageOptions) => Promise; - setEmail: (value: string, options?: StorageOptions) => Promise; - getEmailVerified: (options?: StorageOptions) => Promise; - setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; - getEnableAlwaysOnTop: (options?: StorageOptions) => Promise; - setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise; - getEnableBiometric: (options?: StorageOptions) => Promise; - setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise; - getEnableCloseToTray: (options?: StorageOptions) => Promise; - setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise; - getEnableFullWidth: (options?: StorageOptions) => Promise; - setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise; - getEnableMinimizeToTray: (options?: StorageOptions) => Promise; - setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise; - getEnableStartToTray: (options?: StorageOptions) => Promise; - setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise; - getEnableTray: (options?: StorageOptions) => Promise; - setEnableTray: (value: boolean, options?: StorageOptions) => Promise; - getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; - setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise; - getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise; - setEncryptedOrganizationKeys: ( - value: Map, - options?: StorageOptions, - ) => Promise; - getEncryptedPinProtected: (options?: StorageOptions) => Promise; - setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; - getEncryptedPrivateKey: (options?: StorageOptions) => Promise; - setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise; - getEncryptedProviderKeys: (options?: StorageOptions) => Promise; - setEncryptedProviderKeys: (value: any, options?: StorageOptions) => Promise; - getEntityId: (options?: StorageOptions) => Promise; - getEnvironmentUrls: (options?: StorageOptions) => Promise; - setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise; - getEquivalentDomains: (options?: StorageOptions) => Promise; - setEquivalentDomains: (value: string, options?: StorageOptions) => Promise; - getEverBeenUnlocked: (options?: StorageOptions) => Promise; - setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; - getForcePasswordReset: (options?: StorageOptions) => Promise; - setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise; - getInstalledVersion: (options?: StorageOptions) => Promise; - setInstalledVersion: (value: string, options?: StorageOptions) => Promise; - getIsAuthenticated: (options?: StorageOptions) => Promise; - getKdfIterations: (options?: StorageOptions) => Promise; - setKdfIterations: (value: number, options?: StorageOptions) => Promise; - getKdfType: (options?: StorageOptions) => Promise; - setKdfType: (value: KdfType, options?: StorageOptions) => Promise; - getKeyHash: (options?: StorageOptions) => Promise; - setKeyHash: (value: string, options?: StorageOptions) => Promise; - getLastActive: (options?: StorageOptions) => Promise; - setLastActive: (value: number, options?: StorageOptions) => Promise; - getLastSync: (options?: StorageOptions) => Promise; - setLastSync: (value: string, options?: StorageOptions) => Promise; - getLegacyEtmKey: (options?: StorageOptions) => Promise; - setLegacyEtmKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; - getLocalData: (options?: StorageOptions) => Promise; - setLocalData: (value: string, options?: StorageOptions) => Promise; - getLocale: (options?: StorageOptions) => Promise; - setLocale: (value: string, options?: StorageOptions) => Promise; - getLoginRedirect: (options?: StorageOptions) => Promise; - setLoginRedirect: (value: any, options?: StorageOptions) => Promise; - getMainWindowSize: (options?: StorageOptions) => Promise; - setMainWindowSize: (value: number, options?: StorageOptions) => Promise; - getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; - setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; - getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>; - setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise; - getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise; - setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise; - getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise; - setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise; - getOpenAtLogin: (options?: StorageOptions) => Promise; - setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; - getOrganizationInvitation: (options?: StorageOptions) => Promise; - setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; - getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>; - setOrganizations: ( - value: { [id: string]: OrganizationData }, - options?: StorageOptions, - ) => Promise; - getPasswordGenerationOptions: (options?: StorageOptions) => Promise; - setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise; - getUsernameGenerationOptions: (options?: StorageOptions) => Promise; - setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise; - getGeneratorOptions: (options?: StorageOptions) => Promise; - setGeneratorOptions: (value: any, options?: StorageOptions) => Promise; - getProtectedPin: (options?: StorageOptions) => Promise; - setProtectedPin: (value: string, options?: StorageOptions) => Promise; - getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>; - setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise; - getPublicKey: (options?: StorageOptions) => Promise; - setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise; - getRefreshToken: (options?: StorageOptions) => Promise; - setRefreshToken: (value: string, options?: StorageOptions) => Promise; - getRememberedEmail: (options?: StorageOptions) => Promise; - setRememberedEmail: (value: string, options?: StorageOptions) => Promise; - getSecurityStamp: (options?: StorageOptions) => Promise; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise; - getSettings: (options?: StorageOptions) => Promise; - setSettings: (value: string, options?: StorageOptions) => Promise; - getSsoCodeVerifier: (options?: StorageOptions) => Promise; - setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise; - getSsoOrgIdentifier: (options?: StorageOptions) => Promise; - setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise; - getSsoState: (options?: StorageOptions) => Promise; - setSsoState: (value: string, options?: StorageOptions) => Promise; - getTheme: (options?: StorageOptions) => Promise; - setTheme: (value: ThemeType, options?: StorageOptions) => Promise; - getTwoFactorToken: (options?: StorageOptions) => Promise; - setTwoFactorToken: (value: string, options?: StorageOptions) => Promise; - getUserId: (options?: StorageOptions) => Promise; - getUsesKeyConnector: (options?: StorageOptions) => Promise; - setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise; - getVaultTimeout: (options?: StorageOptions) => Promise; - setVaultTimeout: (value: number, options?: StorageOptions) => Promise; - getVaultTimeoutAction: (options?: StorageOptions) => Promise; - setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; - getStateVersion: () => Promise; - setStateVersion: (value: number) => Promise; - getWindow: () => Promise; - setWindow: (value: WindowState) => Promise; -} +// Stub file - re-exports DC StateService +export { StateService } from "@/src/abstractions/state.service"; diff --git a/jslib/common/src/abstractions/stateMigration.service.ts b/jslib/common/src/abstractions/stateMigration.service.ts deleted file mode 100644 index f16777a1..00000000 --- a/jslib/common/src/abstractions/stateMigration.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -export abstract class StateMigrationService { - needsMigration: () => Promise; - migrate: () => Promise; -} diff --git a/jslib/common/src/abstractions/token.service.ts b/jslib/common/src/abstractions/token.service.ts index c4fcf635..489f4f61 100644 --- a/jslib/common/src/abstractions/token.service.ts +++ b/jslib/common/src/abstractions/token.service.ts @@ -1,32 +1,2 @@ -import { IdentityTokenResponse } from "../models/response/identityTokenResponse"; - -export abstract class TokenService { - setTokens: ( - accessToken: string, - refreshToken: string, - clientIdClientSecret: [string, string], - ) => Promise; - setToken: (token: string) => Promise; - getToken: () => Promise; - setRefreshToken: (refreshToken: string) => Promise; - getRefreshToken: () => Promise; - setClientId: (clientId: string) => Promise; - getClientId: () => Promise; - setClientSecret: (clientSecret: string) => Promise; - getClientSecret: () => Promise; - setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise; - getTwoFactorToken: () => Promise; - clearTwoFactorToken: () => Promise; - clearToken: (userId?: string) => Promise; - decodeToken: (token?: string) => any; - getTokenExpirationDate: () => Promise; - tokenSecondsRemaining: (offsetSeconds?: number) => Promise; - tokenNeedsRefresh: (minutes?: number) => Promise; - getUserId: () => Promise; - getEmail: () => Promise; - getEmailVerified: () => Promise; - getName: () => Promise; - getPremium: () => Promise; - getIssuer: () => Promise; - getIsExternal: () => Promise; -} +// Stub file - re-exports DC TokenService +export { TokenService } from "@/src/abstractions/token.service"; diff --git a/jslib/common/src/factories/accountFactory.ts b/jslib/common/src/factories/accountFactory.ts deleted file mode 100644 index 1fe5aee3..00000000 --- a/jslib/common/src/factories/accountFactory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Account } from "../models/domain/account"; - -export class AccountFactory { - private accountConstructor: new (init: Partial) => T; - - constructor(accountConstructor: new (init: Partial) => T) { - this.accountConstructor = accountConstructor; - } - - create(args: Partial) { - return new this.accountConstructor(args); - } -} diff --git a/jslib/common/src/factories/stateFactory.ts b/jslib/common/src/factories/stateFactory.ts deleted file mode 100644 index 95846348..00000000 --- a/jslib/common/src/factories/stateFactory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Account } from "../models/domain/account"; -import { GlobalState } from "../models/domain/globalState"; - -import { AccountFactory } from "./accountFactory"; -import { GlobalStateFactory } from "./globalStateFactory"; - -export class StateFactory< - TGlobal extends GlobalState = GlobalState, - TAccount extends Account = Account, -> { - private globalStateFactory: GlobalStateFactory; - private accountFactory: AccountFactory; - - constructor( - globalStateConstructor: new (init: Partial) => TGlobal, - accountConstructor: new (init: Partial) => TAccount, - ) { - this.globalStateFactory = new GlobalStateFactory(globalStateConstructor); - this.accountFactory = new AccountFactory(accountConstructor); - } - - createGlobal(args: Partial): TGlobal { - return this.globalStateFactory.create(args); - } - - createAccount(args: Partial): TAccount { - return this.accountFactory.create(args); - } -} diff --git a/jslib/common/src/models/domain/account.ts b/jslib/common/src/models/domain/account.ts deleted file mode 100644 index 9658b46d..00000000 --- a/jslib/common/src/models/domain/account.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { AuthenticationStatus } from "../../enums/authenticationStatus"; -import { KdfType } from "../../enums/kdfType"; -import { UriMatchType } from "../../enums/uriMatchType"; -import { OrganizationData } from "../data/organizationData"; -import { ProviderData } from "../data/providerData"; - -import { EncString } from "./encString"; -import { EnvironmentUrls } from "./environmentUrls"; -import { SymmetricCryptoKey } from "./symmetricCryptoKey"; - -export class EncryptionPair { - encrypted?: TEncrypted; - decrypted?: TDecrypted; -} - -export class DataEncryptionPair { - encrypted?: { [id: string]: TEncrypted }; - decrypted?: TDecrypted[]; -} - -export class AccountData { - ciphers?: any = new DataEncryptionPair(); - folders?: DataEncryptionPair = new DataEncryptionPair(); - localData?: any; - sends?: any = new DataEncryptionPair(); - collections?: DataEncryptionPair = new DataEncryptionPair(); - policies?: DataEncryptionPair = new DataEncryptionPair(); - passwordGenerationHistory?: EncryptionPair = new EncryptionPair(); - addEditCipherInfo?: any; - eventCollection?: any[]; - organizations?: { [id: string]: OrganizationData }; - providers?: { [id: string]: ProviderData }; -} - -export class AccountKeys { - cryptoMasterKey?: SymmetricCryptoKey; - cryptoMasterKeyAuto?: string; - cryptoMasterKeyB64?: string; - cryptoMasterKeyBiometric?: string; - cryptoSymmetricKey?: EncryptionPair = new EncryptionPair< - string, - SymmetricCryptoKey - >(); - organizationKeys?: EncryptionPair> = new EncryptionPair< - any, - Map - >(); - providerKeys?: EncryptionPair> = new EncryptionPair< - any, - Map - >(); - privateKey?: EncryptionPair = new EncryptionPair(); - legacyEtmKey?: SymmetricCryptoKey; - publicKey?: ArrayBuffer; - apiKeyClientSecret?: string; -} - -export class AccountProfile { - apiKeyClientId?: string; - authenticationStatus?: AuthenticationStatus; - convertAccountToKeyConnector?: boolean; - email?: string; - emailVerified?: boolean; - entityId?: string; - entityType?: string; - everBeenUnlocked?: boolean; - forcePasswordReset?: boolean; - hasPremiumPersonally?: boolean; - lastSync?: string; - userId?: string; - usesKeyConnector?: boolean; - keyHash?: string; - kdfIterations?: number; - kdfType?: KdfType; -} - -export class AccountSettings { - autoConfirmFingerPrints?: boolean; - autoFillOnPageLoadDefault?: boolean; - biometricLocked?: boolean; - biometricUnlock?: boolean; - clearClipboard?: number; - collapsedGroupings?: string[]; - defaultUriMatch?: UriMatchType; - disableAddLoginNotification?: boolean; - disableAutoBiometricsPrompt?: boolean; - disableAutoTotpCopy?: boolean; - disableBadgeCounter?: boolean; - disableChangedPasswordNotification?: boolean; - disableContextMenuItem?: boolean; - disableGa?: boolean; - dontShowCardsCurrentTab?: boolean; - dontShowIdentitiesCurrentTab?: boolean; - enableAlwaysOnTop?: boolean; - enableAutoFillOnPageLoad?: boolean; - enableBiometric?: boolean; - enableFullWidth?: boolean; - enableGravitars?: boolean; - environmentUrls: EnvironmentUrls = new EnvironmentUrls(); - equivalentDomains?: any; - minimizeOnCopyToClipboard?: boolean; - neverDomains?: { [id: string]: any }; - passwordGenerationOptions?: any; - usernameGenerationOptions?: any; - generatorOptions?: any; - pinProtected?: EncryptionPair = new EncryptionPair(); - protectedPin?: string; - settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly - vaultTimeout?: number; - vaultTimeoutAction?: string = "lock"; -} - -export class AccountTokens { - accessToken?: string; - decodedToken?: any; - refreshToken?: string; - securityStamp?: string; -} - -export class Account { - data?: AccountData = new AccountData(); - keys?: AccountKeys = new AccountKeys(); - profile?: AccountProfile = new AccountProfile(); - settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); - - constructor(init: Partial) { - Object.assign(this, { - data: { - ...new AccountData(), - ...init?.data, - }, - keys: { - ...new AccountKeys(), - ...init?.keys, - }, - profile: { - ...new AccountProfile(), - ...init?.profile, - }, - settings: { - ...new AccountSettings(), - ...init?.settings, - }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, - }); - } -} diff --git a/jslib/common/src/models/domain/domainBase.ts b/jslib/common/src/models/domain/domainBase.ts deleted file mode 100644 index f730620b..00000000 --- a/jslib/common/src/models/domain/domainBase.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { View } from "../view/view"; - -import { EncString } from "./encString"; -import { SymmetricCryptoKey } from "./symmetricCryptoKey"; - -export default class Domain { - protected buildDomainModel( - domain: D, - dataObj: any, - map: any, - notEncList: any[] = [], - ) { - for (const prop in map) { - // eslint-disable-next-line - if (!map.hasOwnProperty(prop)) { - continue; - } - - const objProp = dataObj[map[prop] || prop]; - if (notEncList.indexOf(prop) > -1) { - (domain as any)[prop] = objProp ? objProp : null; - } else { - (domain as any)[prop] = objProp ? new EncString(objProp) : null; - } - } - } - protected buildDataModel( - domain: D, - dataObj: any, - map: any, - notEncStringList: any[] = [], - ) { - for (const prop in map) { - // eslint-disable-next-line - if (!map.hasOwnProperty(prop)) { - continue; - } - - const objProp = (domain as any)[map[prop] || prop]; - if (notEncStringList.indexOf(prop) > -1) { - (dataObj as any)[prop] = objProp != null ? objProp : null; - } else { - (dataObj as any)[prop] = objProp != null ? (objProp as EncString).encryptedString : null; - } - } - } - - protected async decryptObj( - viewModel: T, - map: any, - orgId: string, - key: SymmetricCryptoKey = null, - ): Promise { - const promises = []; - const self: any = this; - - for (const prop in map) { - // eslint-disable-next-line - if (!map.hasOwnProperty(prop)) { - continue; - } - - (function (theProp) { - const p = Promise.resolve() - .then(() => { - const mapProp = map[theProp] || theProp; - if (self[mapProp]) { - return self[mapProp].decrypt(orgId, key); - } - return null; - }) - .then((val: any) => { - (viewModel as any)[theProp] = val; - }); - promises.push(p); - })(prop); - } - - await Promise.all(promises); - return viewModel; - } -} diff --git a/jslib/common/src/models/domain/encString.ts b/jslib/common/src/models/domain/encString.ts deleted file mode 100644 index 2e73808d..00000000 --- a/jslib/common/src/models/domain/encString.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { CryptoService } from "../../abstractions/crypto.service"; -import { EncryptionType } from "../../enums/encryptionType"; -import { Utils } from "../../misc/utils"; - -import { SymmetricCryptoKey } from "./symmetricCryptoKey"; - -export class EncString { - encryptedString?: string; - encryptionType?: EncryptionType; - decryptedValue?: string; - data?: string; - iv?: string; - mac?: string; - - constructor( - encryptedStringOrType: string | EncryptionType, - data?: string, - iv?: string, - mac?: string, - ) { - if (data != null) { - // data and header - const encType = encryptedStringOrType as EncryptionType; - - if (iv != null) { - this.encryptedString = encType + "." + iv + "|" + data; - } else { - this.encryptedString = encType + "." + data; - } - - // mac - if (mac != null) { - this.encryptedString += "|" + mac; - } - - this.encryptionType = encType; - this.data = data; - this.iv = iv; - this.mac = mac; - - return; - } - - this.encryptedString = encryptedStringOrType as string; - if (!this.encryptedString) { - return; - } - - const headerPieces = this.encryptedString.split("."); - let encPieces: string[] = null; - - if (headerPieces.length === 2) { - try { - this.encryptionType = parseInt(headerPieces[0], null); - encPieces = headerPieces[1].split("|"); - } catch { - return; - } - } else { - encPieces = this.encryptedString.split("|"); - this.encryptionType = - encPieces.length === 3 - ? EncryptionType.AesCbc128_HmacSha256_B64 - : EncryptionType.AesCbc256_B64; - } - - switch (this.encryptionType) { - case EncryptionType.AesCbc128_HmacSha256_B64: - case EncryptionType.AesCbc256_HmacSha256_B64: - if (encPieces.length !== 3) { - return; - } - - this.iv = encPieces[0]; - this.data = encPieces[1]; - this.mac = encPieces[2]; - break; - case EncryptionType.AesCbc256_B64: - if (encPieces.length !== 2) { - return; - } - - this.iv = encPieces[0]; - this.data = encPieces[1]; - break; - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_B64: - if (encPieces.length !== 1) { - return; - } - - this.data = encPieces[0]; - break; - default: - return; - } - } - - async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise { - if (this.decryptedValue != null) { - return this.decryptedValue; - } - - let cryptoService: CryptoService; - const containerService = (Utils.global as any).bitwardenContainerService; - if (containerService) { - cryptoService = containerService.getCryptoService(); - } else { - throw new Error("global bitwardenContainerService not initialized."); - } - - try { - if (key == null) { - key = await cryptoService.getOrgKey(orgId); - } - this.decryptedValue = await cryptoService.decryptToUtf8(this, key); - } catch { - this.decryptedValue = "[error: cannot decrypt]"; - } - return this.decryptedValue; - } -} diff --git a/jslib/common/src/models/domain/environmentUrls.ts b/jslib/common/src/models/domain/environmentUrls.ts index d4fd173c..838a52df 100644 --- a/jslib/common/src/models/domain/environmentUrls.ts +++ b/jslib/common/src/models/domain/environmentUrls.ts @@ -2,9 +2,5 @@ export class EnvironmentUrls { base: string = null; api: string = null; identity: string = null; - icons: string = null; - notifications: string = null; - events: string = null; webVault: string = null; - keyConnector: string = null; } diff --git a/jslib/common/src/models/domain/state.ts b/jslib/common/src/models/domain/state.ts deleted file mode 100644 index db3e4bcb..00000000 --- a/jslib/common/src/models/domain/state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Account } from "./account"; -import { GlobalState } from "./globalState"; - -export class State< - TGlobalState extends GlobalState = GlobalState, - TAccount extends Account = Account, -> { - accounts: { [userId: string]: TAccount } = {}; - globals: TGlobalState; - activeUserId: string; - authenticatedAccounts: string[] = []; - accountActivity: { [userId: string]: number } = {}; - - constructor(globals: TGlobalState) { - this.globals = globals; - } -} diff --git a/jslib/common/src/services/api.service.ts b/jslib/common/src/services/api.service.ts index 64298538..b0850ae1 100644 --- a/jslib/common/src/services/api.service.ts +++ b/jslib/common/src/services/api.service.ts @@ -214,7 +214,10 @@ export class ApiService implements ApiServiceAbstraction { throw new Error("Invalid response received when refreshing api token"); } - await this.tokenService.setToken(response.accessToken); + await this.tokenService.setTokens(response.accessToken, response.refreshToken, [ + clientId, + clientSecret, + ]); } private async send( diff --git a/jslib/common/src/services/container.service.ts b/jslib/common/src/services/container.service.ts deleted file mode 100644 index 2880e7c7..00000000 --- a/jslib/common/src/services/container.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CryptoService } from "../abstractions/crypto.service"; - -export class ContainerService { - constructor(private cryptoService: CryptoService) {} - - // deprecated, use attachToGlobal instead - attachToWindow(win: any) { - this.attachToGlobal(win); - } - - attachToGlobal(global: any) { - if (!global.bitwardenContainerService) { - global.bitwardenContainerService = this; - } - } - - getCryptoService(): CryptoService { - return this.cryptoService; - } -} diff --git a/jslib/common/src/services/crypto.service.ts b/jslib/common/src/services/crypto.service.ts deleted file mode 100644 index 1f78f9c8..00000000 --- a/jslib/common/src/services/crypto.service.ts +++ /dev/null @@ -1,971 +0,0 @@ -import * as bigInt from "big-integer"; - -import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service"; -import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; -import { LogService } from "../abstractions/log.service"; -import { PlatformUtilsService } from "../abstractions/platformUtils.service"; -import { StateService } from "../abstractions/state.service"; -import { EncryptionType } from "../enums/encryptionType"; -import { HashPurpose } from "../enums/hashPurpose"; -import { KdfType } from "../enums/kdfType"; -import { KeySuffixOptions } from "../enums/keySuffixOptions"; -import { sequentialize } from "../misc/sequentialize"; -import { Utils } from "../misc/utils"; -import { EEFLongWordList } from "../misc/wordlist"; -import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; -import { EncString } from "../models/domain/encString"; -import { EncryptedObject } from "../models/domain/encryptedObject"; -import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse"; -import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse"; -import { ProfileProviderResponse } from "../models/response/profileProviderResponse"; - -export class CryptoService implements CryptoServiceAbstraction { - constructor( - private cryptoFunctionService: CryptoFunctionService, - protected platformUtilService: PlatformUtilsService, - protected logService: LogService, - protected stateService: StateService, - ) {} - - async setKey(key: SymmetricCryptoKey, userId?: string): Promise { - await this.stateService.setCryptoMasterKey(key, { userId: userId }); - await this.storeKey(key, userId); - } - - async setKeyHash(keyHash: string): Promise { - await this.stateService.setKeyHash(keyHash); - } - - async setEncKey(encKey: string): Promise { - if (encKey == null) { - return; - } - - await this.stateService.setDecryptedCryptoSymmetricKey(null); - await this.stateService.setEncryptedCryptoSymmetricKey(encKey); - } - - async setEncPrivateKey(encPrivateKey: string): Promise { - if (encPrivateKey == null) { - return; - } - - await this.stateService.setDecryptedPrivateKey(null); - await this.stateService.setEncryptedPrivateKey(encPrivateKey); - } - - async setOrgKeys( - orgs: ProfileOrganizationResponse[], - providerOrgs: ProfileProviderOrganizationResponse[], - ): Promise { - const orgKeys: any = {}; - orgs.forEach((org) => { - orgKeys[org.id] = org.key; - }); - - for (const providerOrg of providerOrgs) { - // Convert provider encrypted keys to user encrypted. - const providerKey = await this.getProviderKey(providerOrg.providerId); - const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey); - orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString; - } - - await this.stateService.setDecryptedOrganizationKeys(null); - return await this.stateService.setEncryptedOrganizationKeys(orgKeys); - } - - async setProviderKeys(providers: ProfileProviderResponse[]): Promise { - const providerKeys: any = {}; - providers.forEach((provider) => { - providerKeys[provider.id] = provider.key; - }); - - await this.stateService.setDecryptedProviderKeys(null); - return await this.stateService.setEncryptedProviderKeys(providerKeys); - } - - async getKey(keySuffix?: KeySuffixOptions, userId?: string): Promise { - const inMemoryKey = await this.stateService.getCryptoMasterKey({ userId: userId }); - - if (inMemoryKey != null) { - return inMemoryKey; - } - - keySuffix ||= KeySuffixOptions.Auto; - const symmetricKey = await this.getKeyFromStorage(keySuffix, userId); - - if (symmetricKey != null) { - // TODO: Refactor here so get key doesn't also set key - this.setKey(symmetricKey, userId); - } - - return symmetricKey; - } - - async getKeyFromStorage( - keySuffix: KeySuffixOptions, - userId?: string, - ): Promise { - const key = await this.retrieveKeyFromStorage(keySuffix, userId); - if (key != null) { - const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); - - if (!(await this.validateKey(symmetricKey))) { - this.logService.warning("Wrong key, throwing away stored key"); - await this.clearSecretKeyStore(userId); - return null; - } - - return symmetricKey; - } - return null; - } - - async getKeyHash(): Promise { - return await this.stateService.getKeyHash(); - } - - async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise { - const storedKeyHash = await this.getKeyHash(); - if (masterPassword != null && storedKeyHash != null) { - const localKeyHash = await this.hashPassword( - masterPassword, - key, - HashPurpose.LocalAuthorization, - ); - if (localKeyHash != null && storedKeyHash === localKeyHash) { - return true; - } - - // TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated - const serverKeyHash = await this.hashPassword( - masterPassword, - key, - HashPurpose.ServerAuthorization, - ); - if (serverKeyHash != null && storedKeyHash === serverKeyHash) { - await this.setKeyHash(localKeyHash); - return true; - } - } - - return false; - } - - @sequentialize(() => "getEncKey") - getEncKey(key: SymmetricCryptoKey = null): Promise { - return this.getEncKeyHelper(key); - } - - async getPublicKey(): Promise { - const inMemoryPublicKey = await this.stateService.getPublicKey(); - if (inMemoryPublicKey != null) { - return inMemoryPublicKey; - } - - const privateKey = await this.getPrivateKey(); - if (privateKey == null) { - return null; - } - - const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); - await this.stateService.setPublicKey(publicKey); - return publicKey; - } - - async getPrivateKey(): Promise { - const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey(); - if (decryptedPrivateKey != null) { - return decryptedPrivateKey; - } - - const encPrivateKey = await this.stateService.getEncryptedPrivateKey(); - if (encPrivateKey == null) { - return null; - } - - const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null); - await this.stateService.setDecryptedPrivateKey(privateKey); - return privateKey; - } - - async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise { - if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey === null) { - throw new Error("No public key available."); - } - const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256"); - const userFingerprint = await this.cryptoFunctionService.hkdfExpand( - keyFingerprint, - userId, - 32, - "sha256", - ); - return this.hashPhrase(userFingerprint); - } - - @sequentialize(() => "getOrgKeys") - async getOrgKeys(): Promise> { - const orgKeys: Map = new Map(); - const decryptedOrganizationKeys = await this.stateService.getDecryptedOrganizationKeys(); - if (decryptedOrganizationKeys != null && decryptedOrganizationKeys.size > 0) { - return decryptedOrganizationKeys; - } - - const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys(); - if (encOrgKeys == null) { - return null; - } - - let setKey = false; - - for (const orgId in encOrgKeys) { - // eslint-disable-next-line - if (!encOrgKeys.hasOwnProperty(orgId)) { - continue; - } - - const decValue = await this.rsaDecrypt(encOrgKeys[orgId]); - orgKeys.set(orgId, new SymmetricCryptoKey(decValue)); - setKey = true; - } - - if (setKey) { - await this.stateService.setDecryptedOrganizationKeys(orgKeys); - } - - return orgKeys; - } - - async getOrgKey(orgId: string): Promise { - if (orgId == null) { - return null; - } - - const orgKeys = await this.getOrgKeys(); - if (orgKeys == null || !orgKeys.has(orgId)) { - return null; - } - - return orgKeys.get(orgId); - } - - @sequentialize(() => "getProviderKeys") - async getProviderKeys(): Promise> { - const providerKeys: Map = new Map(); - const decryptedProviderKeys = await this.stateService.getDecryptedProviderKeys(); - if (decryptedProviderKeys != null && decryptedProviderKeys.size > 0) { - return decryptedProviderKeys; - } - - const encProviderKeys = await this.stateService.getEncryptedProviderKeys(); - if (encProviderKeys == null) { - return null; - } - - let setKey = false; - - for (const orgId in encProviderKeys) { - // eslint-disable-next-line - if (!encProviderKeys.hasOwnProperty(orgId)) { - continue; - } - - const decValue = await this.rsaDecrypt(encProviderKeys[orgId]); - providerKeys.set(orgId, new SymmetricCryptoKey(decValue)); - setKey = true; - } - - if (setKey) { - await this.stateService.setDecryptedProviderKeys(providerKeys); - } - - return providerKeys; - } - - async getProviderKey(providerId: string): Promise { - if (providerId == null) { - return null; - } - - const providerKeys = await this.getProviderKeys(); - if (providerKeys == null || !providerKeys.has(providerId)) { - return null; - } - - return providerKeys.get(providerId); - } - - async hasKey(): Promise { - return ( - (await this.hasKeyInMemory()) || - (await this.hasKeyStored(KeySuffixOptions.Auto)) || - (await this.hasKeyStored(KeySuffixOptions.Biometric)) - ); - } - - async hasKeyInMemory(userId?: string): Promise { - return (await this.stateService.getCryptoMasterKey({ userId: userId })) != null; - } - - async hasKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise { - switch (keySuffix) { - case KeySuffixOptions.Auto: - return (await this.stateService.getCryptoMasterKeyAuto({ userId: userId })) != null; - case KeySuffixOptions.Biometric: - return (await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId })) === true; - default: - return false; - } - } - - async hasEncKey(): Promise { - return (await this.stateService.getEncryptedCryptoSymmetricKey()) != null; - } - - async clearKey(clearSecretStorage = true, userId?: string): Promise { - await this.stateService.setCryptoMasterKey(null, { userId: userId }); - await this.stateService.setLegacyEtmKey(null, { userId: userId }); - if (clearSecretStorage) { - await this.clearSecretKeyStore(userId); - } - } - - async clearStoredKey(keySuffix: KeySuffixOptions) { - if (keySuffix === KeySuffixOptions.Auto) { - await this.stateService.setCryptoMasterKeyAuto(null); - } else { - await this.stateService.setCryptoMasterKeyBiometric(null); - } - } - - async clearKeyHash(userId?: string): Promise { - return await this.stateService.setKeyHash(null, { userId: userId }); - } - - async clearEncKey(memoryOnly?: boolean, userId?: string): Promise { - await this.stateService.setDecryptedCryptoSymmetricKey(null, { userId: userId }); - if (!memoryOnly) { - await this.stateService.setEncryptedCryptoSymmetricKey(null, { userId: userId }); - } - } - - async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise { - const keysToClear: Promise[] = [ - this.stateService.setDecryptedPrivateKey(null, { userId: userId }), - this.stateService.setPublicKey(null, { userId: userId }), - ]; - if (!memoryOnly) { - keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId })); - } - return Promise.all(keysToClear); - } - - async clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise { - await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId }); - if (!memoryOnly) { - await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId }); - } - } - - async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise { - await this.stateService.setDecryptedProviderKeys(null, { userId: userId }); - if (!memoryOnly) { - await this.stateService.setEncryptedProviderKeys(null, { userId: userId }); - } - } - - async clearPinProtectedKey(userId?: string): Promise { - return await this.stateService.setEncryptedPinProtected(null, { userId: userId }); - } - - async clearKeys(userId?: string): Promise { - await this.clearKey(true, userId); - await this.clearKeyHash(userId); - await this.clearOrgKeys(false, userId); - await this.clearProviderKeys(false, userId); - await this.clearEncKey(false, userId); - await this.clearKeyPair(false, userId); - await this.clearPinProtectedKey(userId); - } - - async toggleKey(): Promise { - const key = await this.getKey(); - - await this.setKey(key); - } - - async makeKey( - password: string, - salt: string, - kdf: KdfType, - kdfIterations: number, - ): Promise { - let key: ArrayBuffer = null; - if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { - if (kdfIterations == null) { - kdfIterations = 5000; - } else if (kdfIterations < 5000) { - throw new Error("PBKDF2 iteration minimum is 5000."); - } - key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfIterations); - } else { - throw new Error("Unknown Kdf."); - } - return new SymmetricCryptoKey(key); - } - - async makeKeyFromPin( - pin: string, - salt: string, - kdf: KdfType, - kdfIterations: number, - protectedKeyCs: EncString = null, - ): Promise { - if (protectedKeyCs == null) { - const pinProtectedKey = await this.stateService.getEncryptedPinProtected(); - if (pinProtectedKey == null) { - throw new Error("No PIN protected key found."); - } - protectedKeyCs = new EncString(pinProtectedKey); - } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations); - const decKey = await this.decryptToBytes(protectedKeyCs, pinKey); - return new SymmetricCryptoKey(decKey); - } - - async makeShareKey(): Promise<[EncString, SymmetricCryptoKey]> { - const shareKey = await this.cryptoFunctionService.randomBytes(64); - const publicKey = await this.getPublicKey(); - const encShareKey = await this.rsaEncrypt(shareKey, publicKey); - return [encShareKey, new SymmetricCryptoKey(shareKey)]; - } - - async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> { - const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); - const publicB64 = Utils.fromBufferToB64(keyPair[0]); - const privateEnc = await this.encrypt(keyPair[1], key); - return [publicB64, privateEnc]; - } - - async makePinKey( - pin: string, - salt: string, - kdf: KdfType, - kdfIterations: number, - ): Promise { - const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations); - return await this.stretchKey(pinKey); - } - - async makeSendKey(keyMaterial: ArrayBuffer): Promise { - const sendKey = await this.cryptoFunctionService.hkdf( - keyMaterial, - "bitwarden-send", - "send", - 64, - "sha256", - ); - return new SymmetricCryptoKey(sendKey); - } - - async hashPassword( - password: string, - key: SymmetricCryptoKey, - hashPurpose?: HashPurpose, - ): Promise { - if (key == null) { - key = await this.getKey(); - } - if (password == null || key == null) { - throw new Error("Invalid parameters."); - } - - const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1; - const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations); - return Utils.fromBufferToB64(hash); - } - - async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, EncString]> { - const theKey = await this.getKeyForEncryption(key); - const encKey = await this.cryptoFunctionService.randomBytes(64); - return this.buildEncKey(theKey, encKey); - } - - async remakeEncKey( - key: SymmetricCryptoKey, - encKey?: SymmetricCryptoKey, - ): Promise<[SymmetricCryptoKey, EncString]> { - if (encKey == null) { - encKey = await this.getEncKey(); - } - return this.buildEncKey(key, encKey.key); - } - - async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { - if (plainValue == null) { - return Promise.resolve(null); - } - - let plainBuf: ArrayBuffer; - if (typeof plainValue === "string") { - plainBuf = Utils.fromUtf8ToArray(plainValue).buffer; - } else { - plainBuf = plainValue; - } - - const encObj = await this.aesEncrypt(plainBuf, key); - const iv = Utils.fromBufferToB64(encObj.iv); - const data = Utils.fromBufferToB64(encObj.data); - const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null; - return new EncString(encObj.key.encType, data, iv, mac); - } - - async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { - const encValue = await this.aesEncrypt(plainValue, key); - let macLen = 0; - if (encValue.mac != null) { - macLen = encValue.mac.byteLength; - } - - const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength); - encBytes.set([encValue.key.encType]); - encBytes.set(new Uint8Array(encValue.iv), 1); - if (encValue.mac != null) { - encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength); - } - - encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen); - return new EncArrayBuffer(encBytes.buffer); - } - - async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise { - if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey == null) { - throw new Error("Public key unavailable."); - } - - const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); - return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); - } - - async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise { - const headerPieces = encValue.split("."); - let encType: EncryptionType = null; - let encPieces: string[]; - - if (headerPieces.length === 1) { - encType = EncryptionType.Rsa2048_OaepSha256_B64; - encPieces = [headerPieces[0]]; - } else if (headerPieces.length === 2) { - try { - encType = parseInt(headerPieces[0], null); - encPieces = headerPieces[1].split("|"); - } catch (e) { - this.logService.error(e); - } - } - - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: // HmacSha256 types are deprecated - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error("encType unavailable."); - } - - if (encPieces == null || encPieces.length <= 0) { - throw new Error("encPieces unavailable."); - } - - const data = Utils.fromB64ToArray(encPieces[0]).buffer; - const privateKey = privateKeyValue ?? (await this.getPrivateKey()); - if (privateKey == null) { - throw new Error("No private key."); - } - - let alg: "sha1" | "sha256" = "sha1"; - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - alg = "sha256"; - break; - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error("encType unavailable."); - } - - return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); - } - - async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise { - const iv = Utils.fromB64ToArray(encString.iv).buffer; - const data = Utils.fromB64ToArray(encString.data).buffer; - const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null; - const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key); - if (decipher == null) { - return null; - } - - return decipher; - } - - async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise { - return await this.aesDecryptToUtf8( - encString.encryptionType, - encString.data, - encString.iv, - encString.mac, - key, - ); - } - - async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { - if (encBuf == null) { - throw new Error("no encBuf."); - } - - const encBytes = new Uint8Array(encBuf); - const encType = encBytes[0]; - let ctBytes: Uint8Array = null; - let ivBytes: Uint8Array = null; - let macBytes: Uint8Array = null; - - switch (encType) { - case EncryptionType.AesCbc128_HmacSha256_B64: - case EncryptionType.AesCbc256_HmacSha256_B64: - if (encBytes.length <= 49) { - // 1 + 16 + 32 + ctLength - return null; - } - - ivBytes = encBytes.slice(1, 17); - macBytes = encBytes.slice(17, 49); - ctBytes = encBytes.slice(49); - break; - case EncryptionType.AesCbc256_B64: - if (encBytes.length <= 17) { - // 1 + 16 + ctLength - return null; - } - - ivBytes = encBytes.slice(1, 17); - ctBytes = encBytes.slice(17); - break; - default: - return null; - } - - return await this.aesDecryptToBytes( - encType, - ctBytes.buffer, - ivBytes.buffer, - macBytes != null ? macBytes.buffer : null, - key, - ); - } - - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - async randomNumber(min: number, max: number): Promise { - let rval = 0; - const range = max - min + 1; - const bitsNeeded = Math.ceil(Math.log2(range)); - if (bitsNeeded > 53) { - throw new Error("We cannot generate numbers larger than 53 bits."); - } - - const bytesNeeded = Math.ceil(bitsNeeded / 8); - const mask = Math.pow(2, bitsNeeded) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Fill a byte array with N random numbers - const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded)); - - let p = (bytesNeeded - 1) * 8; - for (let i = 0; i < bytesNeeded; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return this.randomNumber(min, max); - } - - // Return an integer that falls within the range - return min + rval; - } - - async validateKey(key: SymmetricCryptoKey) { - try { - const encPrivateKey = await this.stateService.getEncryptedPrivateKey(); - const encKey = await this.getEncKeyHelper(key); - if (encPrivateKey == null || encKey == null) { - return false; - } - - const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey); - await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); - } catch { - return false; - } - - return true; - } - - // Helpers - protected async storeKey(key: SymmetricCryptoKey, userId?: string) { - 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.setCryptoMasterKeyAuto(null, { userId: userId }); - await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); - } - } - - protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) { - let shouldStoreKey = false; - if (keySuffix === KeySuffixOptions.Auto) { - const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId }); - shouldStoreKey = vaultTimeout == null; - } else if (keySuffix === KeySuffixOptions.Biometric) { - const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId }); - shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage(); - } - return shouldStoreKey; - } - - protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) { - return keySuffix === KeySuffixOptions.Auto - ? await this.stateService.getCryptoMasterKeyAuto({ userId: userId }) - : await this.stateService.getCryptoMasterKeyBiometric({ userId: userId }); - } - - private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise { - const obj = new EncryptedObject(); - obj.key = await this.getKeyForEncryption(key); - obj.iv = await this.cryptoFunctionService.randomBytes(16); - obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey); - - if (obj.key.macKey != null) { - const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength); - macData.set(new Uint8Array(obj.iv), 0); - macData.set(new Uint8Array(obj.data), obj.iv.byteLength); - obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256"); - } - - return obj; - } - - private async aesDecryptToUtf8( - encType: EncryptionType, - data: string, - iv: string, - mac: string, - key: SymmetricCryptoKey, - ): Promise { - const keyForEnc = await this.getKeyForEncryption(key); - const theKey = await this.resolveLegacyKey(encType, keyForEnc); - - if (theKey.macKey != null && mac == null) { - this.logService.error("mac required."); - return null; - } - - if (theKey.encType !== encType) { - this.logService.error("encType unavailable."); - return null; - } - - const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey); - if (fastParams.macKey != null && fastParams.mac != null) { - const computedMac = await this.cryptoFunctionService.hmacFast( - fastParams.macData, - fastParams.macKey, - "sha256", - ); - const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); - if (!macsEqual) { - this.logService.error("mac failed."); - return null; - } - } - - return this.cryptoFunctionService.aesDecryptFast(fastParams); - } - - private async aesDecryptToBytes( - encType: EncryptionType, - data: ArrayBuffer, - iv: ArrayBuffer, - mac: ArrayBuffer, - key: SymmetricCryptoKey, - ): Promise { - const keyForEnc = await this.getKeyForEncryption(key); - const theKey = await this.resolveLegacyKey(encType, keyForEnc); - - if (theKey.macKey != null && mac == null) { - return null; - } - - if (theKey.encType !== encType) { - return null; - } - - if (theKey.macKey != null && mac != null) { - const macData = new Uint8Array(iv.byteLength + data.byteLength); - macData.set(new Uint8Array(iv), 0); - macData.set(new Uint8Array(data), iv.byteLength); - const computedMac = await this.cryptoFunctionService.hmac( - macData.buffer, - theKey.macKey, - "sha256", - ); - if (computedMac === null) { - return null; - } - - const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); - if (!macsMatch) { - this.logService.error("mac failed."); - return null; - } - } - - return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); - } - - private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { - if (key != null) { - return key; - } - - const encKey = await this.getEncKey(); - if (encKey != null) { - return encKey; - } - - return await this.getKey(); - } - - private async resolveLegacyKey( - encType: EncryptionType, - key: SymmetricCryptoKey, - ): Promise { - if ( - encType === EncryptionType.AesCbc128_HmacSha256_B64 && - key.encType === EncryptionType.AesCbc256_B64 - ) { - // Old encrypt-then-mac scheme, make a new key - let legacyKey = await this.stateService.getLegacyEtmKey(); - if (legacyKey == null) { - legacyKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); - await this.stateService.setLegacyEtmKey(legacyKey); - } - return legacyKey; - } - - return key; - } - - private async stretchKey(key: SymmetricCryptoKey): Promise { - const newKey = new Uint8Array(64); - const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256"); - const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256"); - newKey.set(new Uint8Array(encKey)); - newKey.set(new Uint8Array(macKey), 32); - return new SymmetricCryptoKey(newKey.buffer); - } - - private async hashPhrase(hash: ArrayBuffer, minimumEntropy = 64) { - const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); - let numWords = Math.ceil(minimumEntropy / entropyPerWord); - - const hashArr = Array.from(new Uint8Array(hash)); - const entropyAvailable = hashArr.length * 4; - if (numWords * entropyPerWord > entropyAvailable) { - throw new Error("Output entropy of hash function is too small"); - } - - const phrase: string[] = []; - let hashNumber = bigInt.fromArray(hashArr, 256); - while (numWords--) { - const remainder = hashNumber.mod(EEFLongWordList.length); - hashNumber = hashNumber.divide(EEFLongWordList.length); - phrase.push(EEFLongWordList[remainder as any]); - } - return phrase; - } - - private async buildEncKey( - key: SymmetricCryptoKey, - encKey: ArrayBuffer, - ): Promise<[SymmetricCryptoKey, EncString]> { - let encKeyEnc: EncString = null; - if (key.key.byteLength === 32) { - const newKey = await this.stretchKey(key); - encKeyEnc = await this.encrypt(encKey, newKey); - } else if (key.key.byteLength === 64) { - encKeyEnc = await this.encrypt(encKey, key); - } else { - throw new Error("Invalid key size."); - } - return [new SymmetricCryptoKey(encKey), encKeyEnc]; - } - - private async clearSecretKeyStore(userId?: string): Promise { - await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); - await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); - } - - private async getEncKeyHelper(key: SymmetricCryptoKey = null): Promise { - const inMemoryKey = await this.stateService.getDecryptedCryptoSymmetricKey(); - if (inMemoryKey != null) { - return inMemoryKey; - } - - const encKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - if (encKey == null) { - return null; - } - - if (key == null) { - key = await this.getKey(); - } - if (key == null) { - return null; - } - - let decEncKey: ArrayBuffer; - const encKeyCipher = new EncString(encKey); - if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) { - decEncKey = await this.decryptToBytes(encKeyCipher, key); - } else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { - const newKey = await this.stretchKey(key); - decEncKey = await this.decryptToBytes(encKeyCipher, newKey); - } else { - throw new Error("Unsupported encKey type."); - } - if (decEncKey == null) { - return null; - } - const symmetricCryptoKey = new SymmetricCryptoKey(decEncKey); - await this.stateService.setDecryptedCryptoSymmetricKey(symmetricCryptoKey); - return symmetricCryptoKey; - } -} diff --git a/jslib/common/src/services/environment.service.ts b/jslib/common/src/services/environment.service.ts deleted file mode 100644 index 4ae8749f..00000000 --- a/jslib/common/src/services/environment.service.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { concatMap, distinctUntilChanged, Observable, Subject } from "rxjs"; - -import { - EnvironmentService as EnvironmentServiceAbstraction, - Urls, -} from "../abstractions/environment.service"; -import { StateService } from "../abstractions/state.service"; -import { EnvironmentUrls } from "../models/domain/environmentUrls"; - -export class EnvironmentService implements EnvironmentServiceAbstraction { - private readonly urlsSubject = new Subject(); - urls: Observable = this.urlsSubject; - - private baseUrl: string; - private webVaultUrl: string; - private apiUrl: string; - private identityUrl: string; - private iconsUrl: string; - private notificationsUrl: string; - private eventsUrl: string; - private keyConnectorUrl: string; - - constructor(private stateService: StateService) { - this.stateService.activeAccount$ - .pipe( - // Use == here to not trigger on undefined -> null transition - distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId), - concatMap(async () => { - await this.setUrlsFromStorage(); - }), - ) - .subscribe(); - } - - hasBaseUrl() { - return this.baseUrl != null; - } - - getNotificationsUrl() { - if (this.notificationsUrl != null) { - return this.notificationsUrl; - } - - if (this.baseUrl != null) { - return this.baseUrl + "/notifications"; - } - - return "https://notifications.bitwarden.com"; - } - - getWebVaultUrl() { - if (this.webVaultUrl != null) { - return this.webVaultUrl; - } - - if (this.baseUrl) { - return this.baseUrl; - } - return "https://vault.bitwarden.com"; - } - - getSendUrl() { - return this.getWebVaultUrl() === "https://vault.bitwarden.com" - ? "https://send.bitwarden.com/#" - : this.getWebVaultUrl() + "/#/send/"; - } - - getIconsUrl() { - if (this.iconsUrl != null) { - return this.iconsUrl; - } - - if (this.baseUrl) { - return this.baseUrl + "/icons"; - } - - return "https://icons.bitwarden.net"; - } - - getApiUrl() { - if (this.apiUrl != null) { - return this.apiUrl; - } - - if (this.baseUrl) { - return this.baseUrl + "/api"; - } - - return "https://api.bitwarden.com"; - } - - getIdentityUrl() { - if (this.identityUrl != null) { - return this.identityUrl; - } - - if (this.baseUrl) { - return this.baseUrl + "/identity"; - } - - return "https://identity.bitwarden.com"; - } - - getEventsUrl() { - if (this.eventsUrl != null) { - return this.eventsUrl; - } - - if (this.baseUrl) { - return this.baseUrl + "/events"; - } - - return "https://events.bitwarden.com"; - } - - getKeyConnectorUrl() { - return this.keyConnectorUrl; - } - - async setUrlsFromStorage(): Promise { - const urls: any = await this.stateService.getEnvironmentUrls(); - const envUrls = new EnvironmentUrls(); - - this.baseUrl = envUrls.base = urls.base; - this.webVaultUrl = urls.webVault; - this.apiUrl = envUrls.api = urls.api; - this.identityUrl = envUrls.identity = urls.identity; - this.iconsUrl = urls.icons; - this.notificationsUrl = urls.notifications; - this.eventsUrl = envUrls.events = urls.events; - this.keyConnectorUrl = urls.keyConnector; - } - - async setUrls(urls: Urls): Promise { - urls.base = this.formatUrl(urls.base); - urls.webVault = this.formatUrl(urls.webVault); - urls.api = this.formatUrl(urls.api); - urls.identity = this.formatUrl(urls.identity); - urls.icons = this.formatUrl(urls.icons); - urls.notifications = this.formatUrl(urls.notifications); - urls.events = this.formatUrl(urls.events); - urls.keyConnector = this.formatUrl(urls.keyConnector); - - await this.stateService.setEnvironmentUrls({ - base: urls.base, - api: urls.api, - identity: urls.identity, - webVault: urls.webVault, - icons: urls.icons, - notifications: urls.notifications, - events: urls.events, - keyConnector: urls.keyConnector, - }); - - this.baseUrl = urls.base; - this.webVaultUrl = urls.webVault; - this.apiUrl = urls.api; - this.identityUrl = urls.identity; - this.iconsUrl = urls.icons; - this.notificationsUrl = urls.notifications; - this.eventsUrl = urls.events; - this.keyConnectorUrl = urls.keyConnector; - - this.urlsSubject.next(urls); - - return urls; - } - - getUrls() { - return { - base: this.baseUrl, - webVault: this.webVaultUrl, - api: this.apiUrl, - identity: this.identityUrl, - icons: this.iconsUrl, - notifications: this.notificationsUrl, - events: this.eventsUrl, - keyConnector: this.keyConnectorUrl, - }; - } - - private formatUrl(url: string): string { - if (url == null || url === "") { - return null; - } - - url = url.replace(/\/+$/g, ""); - if (!url.startsWith("http://") && !url.startsWith("https://")) { - url = "https://" + url; - } - - return url.trim(); - } -} diff --git a/jslib/common/src/services/state.service.ts b/jslib/common/src/services/state.service.ts deleted file mode 100644 index d1b40929..00000000 --- a/jslib/common/src/services/state.service.ts +++ /dev/null @@ -1,2256 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { LogService } from "../abstractions/log.service"; -import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; -import { StateMigrationService } from "../abstractions/stateMigration.service"; -import { StorageService } from "../abstractions/storage.service"; -import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; -import { KdfType } from "../enums/kdfType"; -import { StorageLocation } from "../enums/storageLocation"; -import { ThemeType } from "../enums/themeType"; -import { UriMatchType } from "../enums/uriMatchType"; -import { StateFactory } from "../factories/stateFactory"; -import { OrganizationData } from "../models/data/organizationData"; -import { ProviderData } from "../models/data/providerData"; -import { Account, AccountData } from "../models/domain/account"; -import { EncString } from "../models/domain/encString"; -import { EnvironmentUrls } from "../models/domain/environmentUrls"; -import { GlobalState } from "../models/domain/globalState"; -import { State } from "../models/domain/state"; -import { StorageOptions } from "../models/domain/storageOptions"; -import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { WindowState } from "../models/domain/windowState"; - -const keys = { - global: "global", - authenticatedAccounts: "authenticatedAccounts", - activeUserId: "activeUserId", - tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication - accountActivity: "accountActivity", -}; - -const partialKeys = { - autoKey: "_masterkey_auto", - biometricKey: "_masterkey_biometric", - masterKey: "_masterkey", -}; - -export class StateService< - TGlobalState extends GlobalState = GlobalState, - TAccount extends Account = Account, -> implements StateServiceAbstraction { - protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({}); - accounts$ = this.accountsSubject.asObservable(); - - protected activeAccountSubject = new BehaviorSubject(null); - activeAccount$ = this.activeAccountSubject.asObservable(); - - protected state: State = new State( - this.createGlobals(), - ); - - private hasBeenInited = false; - - private accountDiskCache: Map; - - constructor( - protected storageService: StorageService, - protected secureStorageService: StorageService, - protected logService: LogService, - protected stateMigrationService: StateMigrationService, - protected stateFactory: StateFactory, - protected useAccountCache: boolean = true, - ) { - this.accountDiskCache = new Map(); - } - - async init(): Promise { - if (this.hasBeenInited) { - return; - } - - if (await this.stateMigrationService.needsMigration()) { - await this.stateMigrationService.migrate(); - } - - await this.initAccountState(); - this.hasBeenInited = true; - } - - async initAccountState() { - this.state.authenticatedAccounts = - (await this.storageService.get(keys.authenticatedAccounts)) ?? []; - for (const i in this.state.authenticatedAccounts) { - if (i != null) { - await this.syncAccountFromDisk(this.state.authenticatedAccounts[i]); - } - } - const storedActiveUser = await this.storageService.get(keys.activeUserId); - if (storedActiveUser != null) { - this.state.activeUserId = storedActiveUser; - } - await this.pushAccounts(); - this.activeAccountSubject.next(this.state.activeUserId); - } - - async syncAccountFromDisk(userId: string) { - if (userId == null) { - return; - } - this.state.accounts[userId] = this.createAccount(); - const diskAccount = await this.getAccountFromDisk({ userId: userId }); - this.state.accounts[userId].profile = diskAccount.profile; - } - - async addAccount(account: TAccount) { - account = await this.setAccountEnvironmentUrls(account); - this.state.authenticatedAccounts.push(account.profile.userId); - await this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts); - this.state.accounts[account.profile.userId] = account; - await this.scaffoldNewAccountStorage(account); - await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); - await this.setActiveUser(account.profile.userId); - this.activeAccountSubject.next(account.profile.userId); - } - - async setActiveUser(userId: string): Promise { - this.clearDecryptedDataForActiveUser(); - this.state.activeUserId = userId; - await this.storageService.save(keys.activeUserId, userId); - this.activeAccountSubject.next(this.state.activeUserId); - await this.pushAccounts(); - } - - async clean(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, this.defaultInMemoryOptions); - await this.deAuthenticateAccount(options.userId); - if (options.userId === this.state.activeUserId) { - await this.dynamicallySetActiveUser(); - } - - await this.removeAccountFromDisk(options?.userId); - this.removeAccountFromMemory(options?.userId); - await this.pushAccounts(); - } - - async getAccessToken(options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - return (await this.getAccount(options))?.tokens?.accessToken; - } - - async setAccessToken(value: string, options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - const account = await this.getAccount(options); - account.tokens.accessToken = value; - await this.saveAccount(account, options); - } - - async getAddEditCipherInfo(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.data?.addEditCipherInfo; - } - - async setAddEditCipherInfo(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.data.addEditCipherInfo = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getAlwaysShowDock(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.alwaysShowDock ?? false - ); - } - - async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.alwaysShowDock = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getApiKeyClientId(options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - return (await this.getAccount(options))?.profile?.apiKeyClientId; - } - - async setApiKeyClientId(value: string, options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - const account = await this.getAccount(options); - account.profile.apiKeyClientId = value; - await this.saveAccount(account, options); - } - - async getApiKeyClientSecret(options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - return (await this.getAccount(options))?.keys?.apiKeyClientSecret; - } - - async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - const account = await this.getAccount(options); - account.keys.apiKeyClientSecret = value; - await this.saveAccount(account, options); - } - - async getAutoConfirmFingerPrints(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.autoConfirmFingerPrints ?? false - ); - } - - async setAutoConfirmFingerprints(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.autoConfirmFingerPrints = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getBiometricAwaitingAcceptance(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.biometricAwaitingAcceptance ?? false - ); - } - - async setBiometricAwaitingAcceptance(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.biometricAwaitingAcceptance = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getBiometricFingerprintValidated(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.biometricFingerprintValidated ?? false - ); - } - - async setBiometricFingerprintValidated(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.biometricFingerprintValidated = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getBiometricLocked(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.settings - ?.biometricLocked ?? false - ); - } - - async setBiometricLocked(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.settings.biometricLocked = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getBiometricText(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.biometricText; - } - - async setBiometricText(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.biometricText = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getBiometricUnlock(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.biometricUnlock ?? false - ); - } - - async setBiometricUnlock(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.biometricUnlock = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getCanAccessPremium(options?: StorageOptions): Promise { - if (!(await this.getIsAuthenticated(options))) { - return false; - } - - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - if (account.profile.hasPremiumPersonally) { - return true; - } - - const organizations = await this.getOrganizations(options); - if (organizations == null) { - return false; - } - - for (const id of Object.keys(organizations)) { - const o = organizations[id]; - if (o.enabled && o.usersGetPremium && !o.isProviderUser) { - return true; - } - } - - return false; - } - - async getClearClipboard(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.settings?.clearClipboard ?? null - ); - } - - async setClearClipboard(value: number, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.clearClipboard = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getCollapsedGroupings(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.collapsedGroupings; - } - - async setCollapsedGroupings(value: string[], options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.collapsedGroupings = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getConvertAccountToKeyConnector(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.convertAccountToKeyConnector; - } - - async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.convertAccountToKeyConnector = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getCryptoMasterKey(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.keys?.cryptoMasterKey; - } - - async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.keys.cryptoMasterKey = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getCryptoMasterKeyAuto(options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "auto" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get(`${options.userId}${partialKeys.autoKey}`, options); - } - - async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "auto" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return; - } - await this.saveSecureStorageKey(partialKeys.autoKey, value, options); - } - - async getCryptoMasterKeyB64(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options?.userId}${partialKeys.masterKey}`, - options, - ); - } - - async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - await this.saveSecureStorageKey(partialKeys.masterKey, value, options); - } - - async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "biometric" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options.userId}${partialKeys.biometricKey}`, - options, - ); - } - - async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "biometric" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return false; - } - return await this.secureStorageService.has( - `${options.userId}${partialKeys.biometricKey}`, - options, - ); - } - - async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "biometric" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return; - } - await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); - } - - async getDecodedToken(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.tokens?.decodedToken; - } - - async setDecodedToken(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.tokens.decodedToken = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.keys?.cryptoSymmetricKey?.decrypted; - } - - async setDecryptedCryptoSymmetricKey( - value: SymmetricCryptoKey, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.keys.cryptoSymmetricKey.decrypted = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getDecryptedOrganizationKeys( - options?: StorageOptions, - ): Promise> { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.keys?.organizationKeys?.decrypted; - } - - async setDecryptedOrganizationKeys( - value: Map, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.keys.organizationKeys.decrypted = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getDecryptedPinProtected(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.settings?.pinProtected?.decrypted; - } - - async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.settings.pinProtected.decrypted = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getDecryptedPrivateKey(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.keys?.privateKey?.decrypted; - } - - async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.keys.privateKey.decrypted = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getDecryptedProviderKeys( - options?: StorageOptions, - ): Promise> { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.keys?.providerKeys?.decrypted; - } - - async setDecryptedProviderKeys( - value: Map, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.keys.providerKeys.decrypted = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getDefaultUriMatch(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.defaultUriMatch; - } - - async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.defaultUriMatch = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableAddLoginNotification(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableAddLoginNotification ?? false - ); - } - - async setDisableAddLoginNotification(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableAddLoginNotification = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableAutoBiometricsPrompt(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableAutoBiometricsPrompt ?? false - ); - } - - async setDisableAutoBiometricsPrompt(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableAutoBiometricsPrompt = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableAutoTotpCopy(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableAutoTotpCopy ?? false - ); - } - - async setDisableAutoTotpCopy(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableAutoTotpCopy = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableBadgeCounter(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableBadgeCounter ?? false - ); - } - - async setDisableBadgeCounter(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableBadgeCounter = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableChangedPasswordNotification(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableChangedPasswordNotification ?? false - ); - } - - async setDisableChangedPasswordNotification( - value: boolean, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableChangedPasswordNotification = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableContextMenuItem(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableContextMenuItem ?? false - ); - } - - async setDisableContextMenuItem(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableContextMenuItem = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDisableFavicon(options?: StorageOptions): Promise { - return ( - ( - await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.disableFavicon ?? false - ); - } - - async setDisableFavicon(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.disableFavicon = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getDisableGa(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableGa ?? false - ); - } - - async setDisableGa(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableGa = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDontShowCardsCurrentTab(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.dontShowCardsCurrentTab ?? false - ); - } - - async setDontShowCardsCurrentTab(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.dontShowCardsCurrentTab = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getDontShowIdentitiesCurrentTab(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.dontShowIdentitiesCurrentTab ?? false - ); - } - - async setDontShowIdentitiesCurrentTab(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.dontShowIdentitiesCurrentTab = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEmail(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.profile?.email; - } - - async setEmail(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.profile.email = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getEmailVerified(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.profile.emailVerified ?? false - ); - } - - async setEmailVerified(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.emailVerified = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableAlwaysOnTop(options?: StorageOptions): Promise { - const accountPreference = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.enableAlwaysOnTop; - const globalPreference = ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.enableAlwaysOnTop; - return accountPreference ?? globalPreference ?? false; - } - - async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.enableAlwaysOnTop = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableAlwaysOnTop = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableAutoFillOnPageLoad(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.enableAutoFillOnPageLoad ?? false - ); - } - - async setEnableAutoFillOnPageLoad(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.enableAutoFillOnPageLoad = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableBiometric(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableBiometrics ?? false - ); - } - - async setEnableBiometric(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableBiometrics = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableBrowserIntegration(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableBrowserIntegration ?? false - ); - } - - async setEnableBrowserIntegration(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableBrowserIntegration = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableBrowserIntegrationFingerprint(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableBrowserIntegrationFingerprint ?? false - ); - } - - async setEnableBrowserIntegrationFingerprint( - value: boolean, - options?: StorageOptions, - ): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableBrowserIntegrationFingerprint = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableCloseToTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableCloseToTray ?? false - ); - } - - async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableCloseToTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableFullWidth(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.settings?.enableFullWidth ?? false - ); - } - - async setEnableFullWidth(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.enableFullWidth = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getEnableGravitars(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.settings?.enableGravitars ?? false - ); - } - - async setEnableGravitars(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.enableGravitars = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getEnableMinimizeToTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableMinimizeToTray ?? false - ); - } - - async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableMinimizeToTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableStartToTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableStartToTray ?? false - ); - } - - async setEnableStartToTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableStartToTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEnableTray(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableTray ?? false - ); - } - - async setEnableTray(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableTray = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys.cryptoSymmetricKey.encrypted; - } - - async setEncryptedCryptoSymmetricKey(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.cryptoSymmetricKey.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEncryptedOrganizationKeys(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys?.organizationKeys.encrypted; - } - - async setEncryptedOrganizationKeys( - value: Map, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.organizationKeys.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEncryptedPinProtected(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.pinProtected?.encrypted; - } - - async setEncryptedPinProtected(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.pinProtected.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEncryptedPrivateKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys?.privateKey?.encrypted; - } - - async setEncryptedPrivateKey(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.privateKey.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEncryptedProviderKeys(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys?.providerKeys?.encrypted; - } - - async setEncryptedProviderKeys(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.providerKeys.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEntityId(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.profile?.entityId; - } - - async getEnvironmentUrls(options?: StorageOptions): Promise { - if (this.state.activeUserId == null) { - return await this.getGlobalEnvironmentUrls(options); - } - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - return (await this.getAccount(options))?.settings?.environmentUrls ?? new EnvironmentUrls(); - } - - async setEnvironmentUrls(value: EnvironmentUrls, options?: StorageOptions): Promise { - // Global values are set on each change and the current global settings are passed to any newly authed accounts. - // This is to allow setting environement values before an account is active, while still allowing individual accounts to have their own environments. - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.environmentUrls = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEquivalentDomains(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.equivalentDomains; - } - - async setEquivalentDomains(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.equivalentDomains = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getEverBeenUnlocked(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile - ?.everBeenUnlocked ?? false - ); - } - - async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.profile.everBeenUnlocked = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getForcePasswordReset(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile - ?.forcePasswordReset ?? false - ); - } - - async setForcePasswordReset(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.profile.forcePasswordReset = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getInstalledVersion(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.installedVersion; - } - - async setInstalledVersion(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.installedVersion = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getIsAuthenticated(options?: StorageOptions): Promise { - return (await this.getAccessToken(options)) != null && (await this.getUserId(options)) != null; - } - - async getKdfIterations(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfIterations; - } - - async setKdfIterations(value: number, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfIterations = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getKdfType(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfType; - } - - async setKdfType(value: KdfType, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getKeyHash(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.keyHash; - } - - async setKeyHash(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.keyHash = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getLastActive(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - - const accountActivity = await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - ); - - if (accountActivity == null || Object.keys(accountActivity).length < 1) { - return null; - } - - return accountActivity[options.userId]; - } - - async setLastActive(value: number, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - if (options.userId == null) { - return; - } - const accountActivity = - (await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - )) ?? {}; - accountActivity[options.userId] = value; - await this.storageService.save(keys.accountActivity, accountActivity, options); - } - - async getLastSync(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.profile?.lastSync; - } - - async setLastSync(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.profile.lastSync = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - - async getLegacyEtmKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys?.legacyEtmKey; - } - - async setLegacyEtmKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.legacyEtmKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getLocalData(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.data?.localData; - } - async setLocalData(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.data.localData = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getLocale(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.locale; - } - - async setLocale(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.locale = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getLoginRedirect(options?: StorageOptions): Promise { - return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.loginRedirect; - } - - async setLoginRedirect(value: any, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - globals.loginRedirect = value; - await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getMainWindowSize(options?: StorageOptions): Promise { - return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.mainWindowSize; - } - - async setMainWindowSize(value: number, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - globals.mainWindowSize = value; - await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.minimizeOnCopyToClipboard ?? false - ); - } - - async setMinimizeOnCopyToClipboard(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.minimizeOnCopyToClipboard = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: any }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.neverDomains; - } - - async setNeverDomains(value: { [id: string]: any }, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.neverDomains = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getNoAutoPromptBiometrics(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.noAutoPromptBiometrics ?? false - ); - } - - async setNoAutoPromptBiometrics(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.noAutoPromptBiometrics = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getNoAutoPromptBiometricsText(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.noAutoPromptBiometricsText; - } - - async setNoAutoPromptBiometricsText(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.noAutoPromptBiometricsText = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getOpenAtLogin(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.openAtLogin ?? false - ); - } - - async setOpenAtLogin(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.openAtLogin = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getOrganizationInvitation(options?: StorageOptions): Promise { - return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.organizationInvitation; - } - - async setOrganizationInvitation(value: any, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - globals.organizationInvitation = value; - await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.data?.organizations; - } - - async setOrganizations( - value: { [id: string]: OrganizationData }, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.data.organizations = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getPasswordGenerationOptions(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.passwordGenerationOptions; - } - - async setPasswordGenerationOptions(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.passwordGenerationOptions = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getUsernameGenerationOptions(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.usernameGenerationOptions; - } - - async setUsernameGenerationOptions(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.usernameGenerationOptions = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getGeneratorOptions(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.generatorOptions; - } - - async setGeneratorOptions(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.generatorOptions = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getProtectedPin(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.protectedPin; - } - - async setProtectedPin(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.protectedPin = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getProviders(options?: StorageOptions): Promise<{ [id: string]: ProviderData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.data?.providers; - } - - async setProviders( - value: { [id: string]: ProviderData }, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.data.providers = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getPublicKey(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.keys?.publicKey; - } - - async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.keys.publicKey = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getRefreshToken(options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - return (await this.getAccount(options))?.tokens?.refreshToken; - } - - async setRefreshToken(value: string, options?: StorageOptions): Promise { - options = await this.getTimeoutBasedStorageOptions(options); - const account = await this.getAccount(options); - account.tokens.refreshToken = value; - await this.saveAccount(account, options); - } - - async getRememberedEmail(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.rememberedEmail; - } - - async setRememberedEmail(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.rememberedEmail = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getSecurityStamp(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.tokens.securityStamp = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); - } - - async getSettings(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.settings?.settings; - } - - async setSettings(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.settings.settings = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - - async getSsoCodeVerifier(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.ssoCodeVerifier; - } - - async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.ssoCodeVerifier = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getSsoOrgIdentifier(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.ssoOrganizationIdentifier; - } - - async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.ssoOrganizationIdentifier = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getSsoState(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.ssoState; - } - - async setSsoState(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.ssoState = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getTheme(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.theme; - } - - async setTheme(value: ThemeType, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.theme = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getTwoFactorToken(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.twoFactorToken; - } - - async setTwoFactorToken(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.twoFactorToken = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getUserId(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.userId; - } - - async getUsesKeyConnector(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.usesKeyConnector; - } - - async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.usesKeyConnector = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getVaultTimeout(options?: StorageOptions): Promise { - const accountVaultTimeout = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.vaultTimeout; - return accountVaultTimeout; - } - - async setVaultTimeout(value: number, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.vaultTimeout = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getVaultTimeoutAction(options?: StorageOptions): Promise { - const accountVaultTimeoutAction = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.vaultTimeoutAction; - const globalVaultTimeoutAction = ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.vaultTimeoutAction; - return accountVaultTimeoutAction ?? globalVaultTimeoutAction; - } - - async setVaultTimeoutAction(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.vaultTimeoutAction = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getStateVersion(): Promise { - return (await this.getGlobals(await this.defaultOnDiskLocalOptions())).stateVersion ?? 1; - } - - async setStateVersion(value: number): Promise { - const globals = await this.getGlobals(await this.defaultOnDiskOptions()); - globals.stateVersion = value; - await this.saveGlobals(globals, await this.defaultOnDiskOptions()); - } - - async getWindow(): Promise { - const globals = await this.getGlobals(await this.defaultOnDiskOptions()); - return globals?.window != null && Object.keys(globals.window).length > 0 - ? globals.window - : new WindowState(); - } - - async setWindow(value: WindowState, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.window = value; - return await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - protected async getGlobals(options: StorageOptions): Promise { - let globals: TGlobalState; - if (this.useMemory(options.storageLocation)) { - globals = this.getGlobalsFromMemory(); - } - - if (this.useDisk && globals == null) { - globals = await this.getGlobalsFromDisk(options); - } - - return globals ?? this.createGlobals(); - } - - protected async saveGlobals(globals: TGlobalState, options: StorageOptions) { - return this.useMemory(options.storageLocation) - ? this.saveGlobalsToMemory(globals) - : await this.saveGlobalsToDisk(globals, options); - } - - protected getGlobalsFromMemory(): TGlobalState { - return this.state.globals; - } - - protected async getGlobalsFromDisk(options: StorageOptions): Promise { - return await this.storageService.get(keys.global, options); - } - - protected saveGlobalsToMemory(globals: TGlobalState): void { - this.state.globals = globals; - } - - protected async saveGlobalsToDisk(globals: TGlobalState, options: StorageOptions): Promise { - if (options.useSecureStorage) { - await this.secureStorageService.save(keys.global, globals, options); - } else { - await this.storageService.save(keys.global, globals, options); - } - } - - protected async getAccount(options: StorageOptions): Promise { - try { - let account: TAccount; - if (this.useMemory(options.storageLocation)) { - account = this.getAccountFromMemory(options); - } - - if (this.useDisk(options.storageLocation) && account == null) { - account = await this.getAccountFromDisk(options); - } - - return account; - } catch (e) { - this.logService.error(e); - } - } - - protected getAccountFromMemory(options: StorageOptions): TAccount { - if (this.state.accounts == null) { - return null; - } - return this.state.accounts[this.getUserIdFromMemory(options)]; - } - - protected getUserIdFromMemory(options: StorageOptions): string { - return options?.userId != null - ? this.state.accounts[options.userId]?.profile?.userId - : this.state.activeUserId; - } - - protected async getAccountFromDisk(options: StorageOptions): Promise { - if (options?.userId == null && this.state.activeUserId == null) { - return null; - } - - if (this.useAccountCache) { - const cachedAccount = this.accountDiskCache.get(options.userId); - if (cachedAccount != null) { - return cachedAccount; - } - } - - const account = options?.useSecureStorage - ? ((await this.secureStorageService.get(options.userId, options)) ?? - (await this.storageService.get( - options.userId, - this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }), - ))) - : await this.storageService.get(options.userId, options); - - if (this.useAccountCache) { - this.accountDiskCache.set(options.userId, account); - } - return account; - } - - protected useMemory(storageLocation: StorageLocation) { - return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both; - } - - protected useDisk(storageLocation: StorageLocation) { - return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both; - } - - protected async saveAccount( - account: TAccount, - options: StorageOptions = { - storageLocation: StorageLocation.Both, - useSecureStorage: false, - }, - ) { - return this.useMemory(options.storageLocation) - ? await this.saveAccountToMemory(account) - : await this.saveAccountToDisk(account, options); - } - - protected async saveAccountToDisk(account: TAccount, options: StorageOptions): Promise { - const storageLocation = options.useSecureStorage - ? this.secureStorageService - : this.storageService; - - await storageLocation.save(`${options.userId}`, account, options); - - if (this.useAccountCache) { - this.accountDiskCache.delete(options.userId); - } - } - - protected async saveAccountToMemory(account: TAccount): Promise { - if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) { - this.state.accounts[account.profile.userId] = account; - } - await this.pushAccounts(); - } - - protected async scaffoldNewAccountStorage(account: TAccount): Promise { - // We don't want to manipulate the referenced in memory account - const deepClone = JSON.parse(JSON.stringify(account)); - await this.scaffoldNewAccountLocalStorage(deepClone); - await this.scaffoldNewAccountSessionStorage(deepClone); - await this.scaffoldNewAccountMemoryStorage(deepClone); - } - - // TODO: There is a tech debt item for splitting up these methods - only Web uses multiple storage locations in its storageService. - // For now these methods exist with some redundancy to facilitate this special web requirement. - protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise { - const storedAccount = await this.getAccount( - this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskLocalOptions(), - ), - ); - // EnvironmentUrls are set before authenticating and should override whatever is stored from any previous session - const environmentUrls = account.settings.environmentUrls; - if (storedAccount?.settings != null) { - account.settings = storedAccount.settings; - } else if (await this.storageService.has(keys.tempAccountSettings)) { - account.settings = await this.storageService.get(keys.tempAccountSettings); - await this.storageService.remove(keys.tempAccountSettings); - } - account.settings.environmentUrls = environmentUrls; - if (account.settings.vaultTimeoutAction === "logOut" && account.settings.vaultTimeout != null) { - account.tokens.accessToken = null; - account.tokens.refreshToken = null; - account.profile.apiKeyClientId = null; - account.keys.apiKeyClientSecret = null; - } - await this.saveAccount( - account, - this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskLocalOptions(), - ), - ); - } - - protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise { - const storedAccount = await this.getAccount( - this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskMemoryOptions(), - ), - ); - if (storedAccount?.settings != null) { - storedAccount.settings.environmentUrls = account.settings.environmentUrls; - account.settings = storedAccount.settings; - } - await this.storageService.save( - account.profile.userId, - account, - await this.defaultOnDiskMemoryOptions(), - ); - await this.saveAccount( - account, - this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskMemoryOptions(), - ), - ); - } - - protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise { - const storedAccount = await this.getAccount( - this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()), - ); - if (storedAccount?.settings != null) { - storedAccount.settings.environmentUrls = account.settings.environmentUrls; - account.settings = storedAccount.settings; - } - await this.storageService.save( - account.profile.userId, - account, - await this.defaultOnDiskMemoryOptions(), - ); - await this.saveAccount( - account, - this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()), - ); - } - // - - protected async pushAccounts(): Promise { - await this.pruneInMemoryAccounts(); - if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) { - this.accountsSubject.next(null); - return; - } - - this.accountsSubject.next(this.state.accounts); - } - - protected reconcileOptions( - requestedOptions: StorageOptions, - defaultOptions: StorageOptions, - ): StorageOptions { - if (requestedOptions == null) { - return defaultOptions; - } - requestedOptions.userId = requestedOptions?.userId ?? defaultOptions.userId; - requestedOptions.storageLocation = - requestedOptions?.storageLocation ?? defaultOptions.storageLocation; - requestedOptions.useSecureStorage = - requestedOptions?.useSecureStorage ?? defaultOptions.useSecureStorage; - requestedOptions.htmlStorageLocation = - requestedOptions?.htmlStorageLocation ?? defaultOptions.htmlStorageLocation; - requestedOptions.keySuffix = requestedOptions?.keySuffix ?? defaultOptions.keySuffix; - return requestedOptions; - } - - protected get defaultInMemoryOptions(): StorageOptions { - return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId }; - } - - protected async defaultOnDiskOptions(): Promise { - return { - storageLocation: StorageLocation.Disk, - htmlStorageLocation: HtmlStorageLocation.Session, - userId: this.state.activeUserId ?? (await this.getActiveUserIdFromStorage()), - useSecureStorage: false, - }; - } - - protected async defaultOnDiskLocalOptions(): Promise { - return { - storageLocation: StorageLocation.Disk, - htmlStorageLocation: HtmlStorageLocation.Local, - userId: this.state.activeUserId ?? (await this.getActiveUserIdFromStorage()), - useSecureStorage: false, - }; - } - - protected async defaultOnDiskMemoryOptions(): Promise { - return { - storageLocation: StorageLocation.Disk, - htmlStorageLocation: HtmlStorageLocation.Memory, - userId: this.state.activeUserId ?? (await this.getUserId()), - useSecureStorage: false, - }; - } - - protected async defaultSecureStorageOptions(): Promise { - return { - storageLocation: StorageLocation.Disk, - useSecureStorage: true, - userId: this.state.activeUserId ?? (await this.getActiveUserIdFromStorage()), - }; - } - - protected async getActiveUserIdFromStorage(): Promise { - return await this.storageService.get(keys.activeUserId); - } - - protected async removeAccountFromLocalStorage( - userId: string = this.state.activeUserId, - ): Promise { - const storedAccount = await this.getAccount( - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), - ); - await this.saveAccount( - this.resetAccount(storedAccount), - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), - ); - } - - protected async removeAccountFromSessionStorage( - userId: string = this.state.activeUserId, - ): Promise { - const storedAccount = await this.getAccount( - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), - ); - await this.saveAccount( - this.resetAccount(storedAccount), - this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), - ); - } - - protected async removeAccountFromSecureStorage( - userId: string = this.state.activeUserId, - ): Promise { - await this.setCryptoMasterKeyAuto(null, { userId: userId }); - await this.setCryptoMasterKeyBiometric(null, { userId: userId }); - await this.setCryptoMasterKeyB64(null, { userId: userId }); - } - - protected removeAccountFromMemory(userId: string = this.state.activeUserId): void { - delete this.state.accounts[userId]; - if (this.useAccountCache) { - this.accountDiskCache.delete(userId); - } - } - - 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 }))) { - this.removeAccountFromMemory(userId); - } - } - } - - // settings persist even on reset, and are not effected by this method - protected resetAccount(account: TAccount) { - const persistentAccountInformation = { settings: account.settings }; - return Object.assign(this.createAccount(), persistentAccountInformation); - } - - protected async setAccountEnvironmentUrls(account: TAccount): Promise { - account.settings.environmentUrls = await this.getGlobalEnvironmentUrls(); - return account; - } - - protected async getGlobalEnvironmentUrls(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls(); - } - - protected clearDecryptedDataForActiveUser() { - const userId = this.state.activeUserId; - if (userId == null || this.state?.accounts[userId]?.data == null) { - return; - } - this.state.accounts[userId].data = new AccountData(); - } - - protected createAccount(init: Partial = null): TAccount { - return this.stateFactory.createAccount(init); - } - - protected createGlobals(init: Partial = null): TGlobalState { - return this.stateFactory.createGlobal(init); - } - - protected async deAuthenticateAccount(userId: string) { - await this.setAccessToken(null, { userId: userId }); - await this.setLastActive(null, { userId: userId }); - this.state.authenticatedAccounts = this.state.authenticatedAccounts.filter( - (activeUserId) => activeUserId !== userId, - ); - await this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts); - } - - protected async removeAccountFromDisk(userId: string) { - await this.removeAccountFromSessionStorage(userId); - await this.removeAccountFromLocalStorage(userId); - await this.removeAccountFromSecureStorage(userId); - } - - protected async dynamicallySetActiveUser() { - if (this.state.accounts == null || Object.keys(this.state.accounts).length < 1) { - await this.setActiveUser(null); - return; - } - for (const userId in this.state.accounts) { - if (userId == null) { - continue; - } - if (await this.getIsAuthenticated({ userId: userId })) { - await this.setActiveUser(userId); - break; - } - await this.setActiveUser(null); - } - } - - private async getTimeoutBasedStorageOptions(options?: StorageOptions): Promise { - const timeoutAction = await this.getVaultTimeoutAction({ userId: options?.userId }); - const timeout = await this.getVaultTimeout({ userId: options?.userId }); - const defaultOptions = - timeoutAction === "logOut" && timeout != null - ? this.defaultInMemoryOptions - : await this.defaultOnDiskOptions(); - return this.reconcileOptions(options, defaultOptions); - } - - private async saveSecureStorageKey(key: string, value: string, options?: StorageOptions) { - return value == null - ? await this.secureStorageService.remove(`${options.userId}${key}`, options) - : await this.secureStorageService.save(`${options.userId}${key}`, value, options); - } -} diff --git a/jslib/common/src/services/stateMigration.service.ts b/jslib/common/src/services/stateMigration.service.ts deleted file mode 100644 index 723a2a6d..00000000 --- a/jslib/common/src/services/stateMigration.service.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { StorageService } from "../abstractions/storage.service"; -import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; -import { KdfType } from "../enums/kdfType"; -import { StateVersion } from "../enums/stateVersion"; -import { ThemeType } from "../enums/themeType"; -import { StateFactory } from "../factories/stateFactory"; -import { OrganizationData } from "../models/data/organizationData"; -import { ProviderData } from "../models/data/providerData"; -import { Account, AccountSettings } from "../models/domain/account"; -import { EnvironmentUrls } from "../models/domain/environmentUrls"; -import { GlobalState } from "../models/domain/globalState"; -import { StorageOptions } from "../models/domain/storageOptions"; - -import { TokenService } from "./token.service"; - -// 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. -const v1Keys: { [key: string]: string } = { - accessToken: "accessToken", - alwaysShowDock: "alwaysShowDock", - autoConfirmFingerprints: "autoConfirmFingerprints", - autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault", - biometricAwaitingAcceptance: "biometricAwaitingAcceptance", - biometricFingerprintValidated: "biometricFingerprintValidated", - biometricText: "biometricText", - biometricUnlock: "biometric", - clearClipboard: "clearClipboardKey", - clientId: "apikey_clientId", - clientSecret: "apikey_clientSecret", - collapsedGroupings: "collapsedGroupings", - convertAccountToKeyConnector: "convertAccountToKeyConnector", - defaultUriMatch: "defaultUriMatch", - disableAddLoginNotification: "disableAddLoginNotification", - disableAutoBiometricsPrompt: "noAutoPromptBiometrics", - disableAutoTotpCopy: "disableAutoTotpCopy", - disableBadgeCounter: "disableBadgeCounter", - disableChangedPasswordNotification: "disableChangedPasswordNotification", - disableContextMenuItem: "disableContextMenuItem", - disableFavicon: "disableFavicon", - disableGa: "disableGa", - dontShowCardsCurrentTab: "dontShowCardsCurrentTab", - dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab", - emailVerified: "emailVerified", - enableAlwaysOnTop: "enableAlwaysOnTopKey", - enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad", - enableBiometric: "enabledBiometric", - enableBrowserIntegration: "enableBrowserIntegration", - enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint", - enableCloseToTray: "enableCloseToTray", - enableFullWidth: "enableFullWidth", - enableGravatars: "enableGravatars", - enableMinimizeToTray: "enableMinimizeToTray", - enableStartToTray: "enableStartToTrayKey", - enableTray: "enableTray", - encKey: "encKey", // Generated Symmetric Key - encOrgKeys: "encOrgKeys", - encPrivate: "encPrivateKey", - encProviderKeys: "encProviderKeys", - entityId: "entityId", - entityType: "entityType", - environmentUrls: "environmentUrls", - equivalentDomains: "equivalentDomains", - eventCollection: "eventCollection", - forcePasswordReset: "forcePasswordReset", - history: "generatedPasswordHistory", - installedVersion: "installedVersion", - kdf: "kdf", - kdfIterations: "kdfIterations", - key: "key", // Master Key - keyHash: "keyHash", - lastActive: "lastActive", - localData: "sitesLocalData", - locale: "locale", - mainWindowSize: "mainWindowSize", - minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey", - neverDomains: "neverDomains", - noAutoPromptBiometricsText: "noAutoPromptBiometricsText", - openAtLogin: "openAtLogin", - passwordGenerationOptions: "passwordGenerationOptions", - pinProtected: "pinProtectedKey", - protectedPin: "protectedPin", - refreshToken: "refreshToken", - ssoCodeVerifier: "ssoCodeVerifier", - ssoIdentifier: "ssoOrgIdentifier", - ssoState: "ssoState", - stamp: "securityStamp", - theme: "theme", - userEmail: "userEmail", - userId: "userId", - usesConnector: "usesKeyConnector", - vaultTimeoutAction: "vaultTimeoutAction", - vaultTimeout: "lockOption", - rememberedEmail: "rememberedEmail", -}; - -const v1KeyPrefixes: { [key: string]: string } = { - ciphers: "ciphers_", - collections: "collections_", - folders: "folders_", - lastSync: "lastSync_", - policies: "policies_", - twoFactorToken: "twoFactorToken_", - organizations: "organizations_", - providers: "providers_", - sends: "sends_", - settings: "settings_", -}; - -const keys = { - global: "global", - authenticatedAccounts: "authenticatedAccounts", - activeUserId: "activeUserId", - tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication - accountActivity: "accountActivity", -}; - -const partialKeys = { - autoKey: "_masterkey_auto", - biometricKey: "_masterkey_biometric", - masterKey: "_masterkey", -}; - -export class StateMigrationService< - TGlobalState extends GlobalState = GlobalState, - TAccount extends Account = Account, -> { - constructor( - protected storageService: StorageService, - protected secureStorageService: StorageService, - protected stateFactory: StateFactory, - ) {} - - async needsMigration(): Promise { - const currentStateVersion = await this.getCurrentStateVersion(); - return currentStateVersion == null || currentStateVersion < StateVersion.Latest; - } - - async migrate(): Promise { - let currentStateVersion = await this.getCurrentStateVersion(); - while (currentStateVersion < StateVersion.Latest) { - switch (currentStateVersion) { - case StateVersion.One: - await this.migrateStateFrom1To2(); - break; - case StateVersion.Two: - await this.migrateStateFrom2To3(); - break; - case StateVersion.Three: - await this.migrateStateFrom3To4(); - break; - } - - currentStateVersion += 1; - } - } - - protected async migrateStateFrom1To2(): Promise { - const clearV1Keys = async (clearingUserId?: string) => { - for (const key in v1Keys) { - if (key == null) { - continue; - } - await this.set(v1Keys[key], null); - } - if (clearingUserId != null) { - for (const keyPrefix in v1KeyPrefixes) { - if (keyPrefix == null) { - continue; - } - await this.set(v1KeyPrefixes[keyPrefix] + userId, null); - } - } - }; - - // Some processes, like biometrics, may have already defined a value before migrations are run. - // We don't want to null out those values if they don't exist in the old storage scheme (like for new installs) - // So, the OOO for migration is that we: - // 1. Check for an existing storage value from the old storage structure OR - // 2. Check for a value already set by processes that run before migration OR - // 3. Assign the default value - const globals = - (await this.get(keys.global)) ?? this.stateFactory.createGlobal(null); - globals.stateVersion = StateVersion.Two; - globals.environmentUrls = - (await this.get(v1Keys.environmentUrls)) ?? globals.environmentUrls; - globals.locale = (await this.get(v1Keys.locale)) ?? globals.locale; - globals.noAutoPromptBiometrics = - (await this.get(v1Keys.disableAutoBiometricsPrompt)) ?? - globals.noAutoPromptBiometrics; - globals.noAutoPromptBiometricsText = - (await this.get(v1Keys.noAutoPromptBiometricsText)) ?? - globals.noAutoPromptBiometricsText; - globals.ssoCodeVerifier = - (await this.get(v1Keys.ssoCodeVerifier)) ?? globals.ssoCodeVerifier; - globals.ssoOrganizationIdentifier = - (await this.get(v1Keys.ssoIdentifier)) ?? globals.ssoOrganizationIdentifier; - globals.ssoState = (await this.get(v1Keys.ssoState)) ?? globals.ssoState; - globals.rememberedEmail = - (await this.get(v1Keys.rememberedEmail)) ?? globals.rememberedEmail; - globals.theme = (await this.get(v1Keys.theme)) ?? globals.theme; - globals.vaultTimeout = (await this.get(v1Keys.vaultTimeout)) ?? globals.vaultTimeout; - globals.vaultTimeoutAction = - (await this.get(v1Keys.vaultTimeoutAction)) ?? globals.vaultTimeoutAction; - globals.window = (await this.get(v1Keys.mainWindowSize)) ?? globals.window; - globals.enableTray = (await this.get(v1Keys.enableTray)) ?? globals.enableTray; - globals.enableMinimizeToTray = - (await this.get(v1Keys.enableMinimizeToTray)) ?? globals.enableMinimizeToTray; - globals.enableCloseToTray = - (await this.get(v1Keys.enableCloseToTray)) ?? globals.enableCloseToTray; - globals.enableStartToTray = - (await this.get(v1Keys.enableStartToTray)) ?? globals.enableStartToTray; - globals.openAtLogin = (await this.get(v1Keys.openAtLogin)) ?? globals.openAtLogin; - globals.alwaysShowDock = - (await this.get(v1Keys.alwaysShowDock)) ?? globals.alwaysShowDock; - globals.enableBrowserIntegration = - (await this.get(v1Keys.enableBrowserIntegration)) ?? - globals.enableBrowserIntegration; - globals.enableBrowserIntegrationFingerprint = - (await this.get(v1Keys.enableBrowserIntegrationFingerprint)) ?? - globals.enableBrowserIntegrationFingerprint; - - const userId = - (await this.get(v1Keys.userId)) ?? (await this.get(v1Keys.entityId)); - - const defaultAccount = this.stateFactory.createAccount(null); - const accountSettings: AccountSettings = { - autoConfirmFingerPrints: - (await this.get(v1Keys.autoConfirmFingerprints)) ?? - defaultAccount.settings.autoConfirmFingerPrints, - autoFillOnPageLoadDefault: - (await this.get(v1Keys.autoFillOnPageLoadDefault)) ?? - defaultAccount.settings.autoFillOnPageLoadDefault, - biometricLocked: null, - biometricUnlock: - (await this.get(v1Keys.biometricUnlock)) ?? - defaultAccount.settings.biometricUnlock, - clearClipboard: - (await this.get(v1Keys.clearClipboard)) ?? defaultAccount.settings.clearClipboard, - defaultUriMatch: - (await this.get(v1Keys.defaultUriMatch)) ?? defaultAccount.settings.defaultUriMatch, - disableAddLoginNotification: - (await this.get(v1Keys.disableAddLoginNotification)) ?? - defaultAccount.settings.disableAddLoginNotification, - disableAutoBiometricsPrompt: - (await this.get(v1Keys.disableAutoBiometricsPrompt)) ?? - defaultAccount.settings.disableAutoBiometricsPrompt, - disableAutoTotpCopy: - (await this.get(v1Keys.disableAutoTotpCopy)) ?? - defaultAccount.settings.disableAutoTotpCopy, - disableBadgeCounter: - (await this.get(v1Keys.disableBadgeCounter)) ?? - defaultAccount.settings.disableBadgeCounter, - disableChangedPasswordNotification: - (await this.get(v1Keys.disableChangedPasswordNotification)) ?? - defaultAccount.settings.disableChangedPasswordNotification, - disableContextMenuItem: - (await this.get(v1Keys.disableContextMenuItem)) ?? - defaultAccount.settings.disableContextMenuItem, - disableGa: (await this.get(v1Keys.disableGa)) ?? defaultAccount.settings.disableGa, - dontShowCardsCurrentTab: - (await this.get(v1Keys.dontShowCardsCurrentTab)) ?? - defaultAccount.settings.dontShowCardsCurrentTab, - dontShowIdentitiesCurrentTab: - (await this.get(v1Keys.dontShowIdentitiesCurrentTab)) ?? - defaultAccount.settings.dontShowIdentitiesCurrentTab, - enableAlwaysOnTop: - (await this.get(v1Keys.enableAlwaysOnTop)) ?? - defaultAccount.settings.enableAlwaysOnTop, - enableAutoFillOnPageLoad: - (await this.get(v1Keys.enableAutoFillOnPageLoad)) ?? - defaultAccount.settings.enableAutoFillOnPageLoad, - enableBiometric: - (await this.get(v1Keys.enableBiometric)) ?? - defaultAccount.settings.enableBiometric, - enableFullWidth: - (await this.get(v1Keys.enableFullWidth)) ?? - defaultAccount.settings.enableFullWidth, - enableGravitars: - (await this.get(v1Keys.enableGravatars)) ?? - defaultAccount.settings.enableGravitars, - environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls, - equivalentDomains: - (await this.get(v1Keys.equivalentDomains)) ?? - defaultAccount.settings.equivalentDomains, - minimizeOnCopyToClipboard: - (await this.get(v1Keys.minimizeOnCopyToClipboard)) ?? - defaultAccount.settings.minimizeOnCopyToClipboard, - neverDomains: - (await this.get(v1Keys.neverDomains)) ?? defaultAccount.settings.neverDomains, - passwordGenerationOptions: - (await this.get(v1Keys.passwordGenerationOptions)) ?? - defaultAccount.settings.passwordGenerationOptions, - pinProtected: { - decrypted: null, - encrypted: await this.get(v1Keys.pinProtected), - }, - protectedPin: await this.get(v1Keys.protectedPin), - settings: userId == null ? null : await this.get(v1KeyPrefixes.settings + userId), - vaultTimeout: - (await this.get(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout, - vaultTimeoutAction: - (await this.get(v1Keys.vaultTimeoutAction)) ?? - defaultAccount.settings.vaultTimeoutAction, - }; - - // (userId == null) = no logged in user (so no known userId) and we need to temporarily store account specific settings in state to migrate on first auth - // (userId != null) = we have a currently authed user (so known userId) with encrypted data and other key settings we can move, no need to temporarily store account settings - if (userId == null) { - await this.set(keys.tempAccountSettings, accountSettings); - await this.set(keys.global, globals); - await this.set(keys.authenticatedAccounts, []); - await this.set(keys.activeUserId, null); - await clearV1Keys(); - return; - } - - globals.twoFactorToken = await this.get(v1KeyPrefixes.twoFactorToken + userId); - await this.set(keys.global, globals); - await this.set(userId, { - data: { - addEditCipherInfo: null, - ciphers: { - decrypted: null, - encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.ciphers + userId), - }, - collapsedGroupings: null, - collections: { - decrypted: null, - encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.collections + userId), - }, - eventCollection: await this.get(v1Keys.eventCollection), - folders: { - decrypted: null, - encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.folders + userId), - }, - localData: null, - organizations: await this.get<{ [id: string]: OrganizationData }>( - v1KeyPrefixes.organizations + userId, - ), - passwordGenerationHistory: { - decrypted: null, - encrypted: await this.get(v1Keys.history), - }, - policies: { - decrypted: null, - encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.policies + userId), - }, - providers: await this.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId), - sends: { - decrypted: null, - encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.sends + userId), - }, - }, - keys: { - apiKeyClientSecret: await this.get(v1Keys.clientSecret), - cryptoMasterKey: null, - cryptoMasterKeyAuto: null, - cryptoMasterKeyB64: null, - cryptoMasterKeyBiometric: null, - cryptoSymmetricKey: { - encrypted: await this.get(v1Keys.encKey), - decrypted: null, - }, - legacyEtmKey: null, - organizationKeys: { - decrypted: null, - encrypted: await this.get(v1Keys.encOrgKeys), - }, - privateKey: { - decrypted: null, - encrypted: await this.get(v1Keys.encPrivate), - }, - providerKeys: { - decrypted: null, - encrypted: await this.get(v1Keys.encProviderKeys), - }, - publicKey: null, - }, - profile: { - apiKeyClientId: await this.get(v1Keys.clientId), - authenticationStatus: null, - convertAccountToKeyConnector: await this.get(v1Keys.convertAccountToKeyConnector), - email: await this.get(v1Keys.userEmail), - emailVerified: await this.get(v1Keys.emailVerified), - entityId: null, - entityType: null, - everBeenUnlocked: null, - forcePasswordReset: null, - hasPremiumPersonally: null, - kdfIterations: await this.get(v1Keys.kdfIterations), - kdfType: await this.get(v1Keys.kdf), - keyHash: await this.get(v1Keys.keyHash), - lastSync: null, - userId: userId, - usesKeyConnector: null, - }, - settings: accountSettings, - tokens: { - accessToken: await this.get(v1Keys.accessToken), - decodedToken: null, - refreshToken: await this.get(v1Keys.refreshToken), - securityStamp: null, - }, - }); - - await this.set(keys.authenticatedAccounts, [userId]); - await this.set(keys.activeUserId, userId); - - const accountActivity: { [userId: string]: number } = { - [userId]: await this.get(v1Keys.lastActive), - }; - accountActivity[userId] = await this.get(v1Keys.lastActive); - await this.set(keys.accountActivity, accountActivity); - - await clearV1Keys(userId); - - if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) { - await this.secureStorageService.save( - `${userId}${partialKeys.biometricKey}`, - await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }), - { keySuffix: "biometric" }, - ); - await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" }); - } - - if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) { - await this.secureStorageService.save( - `${userId}${partialKeys.autoKey}`, - await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }), - { keySuffix: "auto" }, - ); - await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" }); - } - - if (await this.secureStorageService.has(v1Keys.key)) { - await this.secureStorageService.save( - `${userId}${partialKeys.masterKey}`, - await this.secureStorageService.get(v1Keys.key), - ); - await this.secureStorageService.remove(v1Keys.key); - } - } - - protected async migrateStateFrom2To3(): Promise { - const authenticatedUserIds = await this.get(keys.authenticatedAccounts); - await Promise.all( - authenticatedUserIds.map(async (userId) => { - const account = await this.get(userId); - if ( - account?.profile?.hasPremiumPersonally === null && - account.tokens?.accessToken != null - ) { - const decodedToken = await TokenService.decodeToken(account.tokens.accessToken); - account.profile.hasPremiumPersonally = decodedToken.premium; - await this.set(userId, account); - } - }), - ); - - const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Three; - await this.set(keys.global, globals); - } - - protected async migrateStateFrom3To4(): Promise { - const authenticatedUserIds = await this.get(keys.authenticatedAccounts); - await Promise.all( - authenticatedUserIds.map(async (userId) => { - const account = await this.get(userId); - if (account?.profile?.everBeenUnlocked != null) { - delete account.profile.everBeenUnlocked; - return this.set(userId, account); - } - }), - ); - - const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Four; - await this.set(keys.global, globals); - } - - protected get options(): StorageOptions { - return { htmlStorageLocation: HtmlStorageLocation.Local }; - } - - protected get(key: string): Promise { - return this.storageService.get(key, this.options); - } - - protected set(key: string, value: any): Promise { - if (value == null) { - return this.storageService.remove(key, this.options); - } - return this.storageService.save(key, value, this.options); - } - - protected async getGlobals(): Promise { - return await this.get(keys.global); - } - - protected async getCurrentStateVersion(): Promise { - return (await this.getGlobals())?.stateVersion ?? StateVersion.One; - } -} diff --git a/jslib/common/src/services/token.service.ts b/jslib/common/src/services/token.service.ts deleted file mode 100644 index cfe0c9ad..00000000 --- a/jslib/common/src/services/token.service.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { StateService } from "../abstractions/state.service"; -import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; -import { Utils } from "../misc/utils"; -import { IdentityTokenResponse } from "../models/response/identityTokenResponse"; - -export class TokenService implements TokenServiceAbstraction { - static decodeToken(token: string): Promise { - if (token == null) { - throw new Error("Token not provided."); - } - - const parts = token.split("."); - if (parts.length !== 3) { - throw new Error("JWT must have 3 parts"); - } - - const decoded = Utils.fromUrlB64ToUtf8(parts[1]); - if (decoded == null) { - throw new Error("Cannot decode the token"); - } - - const decodedToken = JSON.parse(decoded); - return decodedToken; - } - - constructor(private stateService: StateService) {} - - async setTokens( - accessToken: string, - refreshToken: string, - clientIdClientSecret: [string, string], - ): Promise { - await this.setToken(accessToken); - await this.setRefreshToken(refreshToken); - if (clientIdClientSecret != null) { - await this.setClientId(clientIdClientSecret[0]); - await this.setClientSecret(clientIdClientSecret[1]); - } - } - - async setClientId(clientId: string): Promise { - return await this.stateService.setApiKeyClientId(clientId); - } - - async getClientId(): Promise { - return await this.stateService.getApiKeyClientId(); - } - - async setClientSecret(clientSecret: string): Promise { - return await this.stateService.setApiKeyClientSecret(clientSecret); - } - - async getClientSecret(): Promise { - return await this.stateService.getApiKeyClientSecret(); - } - - async setToken(token: string): Promise { - await this.stateService.setAccessToken(token); - } - - async getToken(): Promise { - return await this.stateService.getAccessToken(); - } - - async setRefreshToken(refreshToken: string): Promise { - return await this.stateService.setRefreshToken(refreshToken); - } - - async getRefreshToken(): Promise { - return await this.stateService.getRefreshToken(); - } - - async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise { - return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken); - } - - async getTwoFactorToken(): Promise { - return await this.stateService.getTwoFactorToken(); - } - - async clearTwoFactorToken(): Promise { - return await this.stateService.setTwoFactorToken(null); - } - - async clearToken(userId?: string): Promise { - await this.stateService.setAccessToken(null, { userId: userId }); - await this.stateService.setRefreshToken(null, { userId: userId }); - await this.stateService.setApiKeyClientId(null, { userId: userId }); - await this.stateService.setApiKeyClientSecret(null, { userId: userId }); - } - - // jwthelper methods - // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - - async decodeToken(token?: string): Promise { - const storedToken = await this.stateService.getDecodedToken(); - if (token === null && storedToken != null) { - return storedToken; - } - - token = token ?? (await this.stateService.getAccessToken()); - - if (token == null) { - throw new Error("Token not found."); - } - - return TokenService.decodeToken(token); - } - - async getTokenExpirationDate(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.exp === "undefined") { - return null; - } - - const d = new Date(0); // The 0 here is the key, which sets the date to the epoch - d.setUTCSeconds(decoded.exp); - return d; - } - - async tokenSecondsRemaining(offsetSeconds = 0): Promise { - const d = await this.getTokenExpirationDate(); - if (d == null) { - return 0; - } - - const msRemaining = d.valueOf() - (new Date().valueOf() + offsetSeconds * 1000); - return Math.round(msRemaining / 1000); - } - - async tokenNeedsRefresh(minutes = 5): Promise { - const sRemaining = await this.tokenSecondsRemaining(); - return sRemaining < 60 * minutes; - } - - async getUserId(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.sub === "undefined") { - throw new Error("No user id found"); - } - - return decoded.sub as string; - } - - async getEmail(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.email === "undefined") { - throw new Error("No email found"); - } - - return decoded.email as string; - } - - async getEmailVerified(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.email_verified === "undefined") { - throw new Error("No email verification found"); - } - - return decoded.email_verified as boolean; - } - - async getName(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.name === "undefined") { - return null; - } - - return decoded.name as string; - } - - async getPremium(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.premium === "undefined") { - return false; - } - - return decoded.premium as boolean; - } - - async getIssuer(): Promise { - const decoded = await this.decodeToken(); - if (typeof decoded.iss === "undefined") { - throw new Error("No issuer found"); - } - - return decoded.iss as string; - } - - async getIsExternal(): Promise { - const decoded = await this.decodeToken(); - - return Array.isArray(decoded.amr) && decoded.amr.includes("external"); - } -} diff --git a/jslib/electron/src/services/electronPlatformUtils.service.ts b/jslib/electron/src/services/electronPlatformUtils.service.ts index 4ecad8cf..e1aa2ed4 100644 --- a/jslib/electron/src/services/electronPlatformUtils.service.ts +++ b/jslib/electron/src/services/electronPlatformUtils.service.ts @@ -180,7 +180,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } async supportsBiometric(): Promise { - return await this.stateService.getEnableBiometric(); + return Promise.resolve(false); } authenticateBiometric(): Promise { @@ -203,12 +203,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } async getEffectiveTheme() { - const theme = await this.stateService.getTheme(); - if (theme == null || theme === ThemeType.System) { - return this.getDefaultSystemTheme(); - } else { - return theme; - } + return this.getDefaultSystemTheme(); } supportsSecureStorage(): boolean { diff --git a/jslib/electron/src/tray.main.ts b/jslib/electron/src/tray.main.ts index 62dec7de..f186cb87 100644 --- a/jslib/electron/src/tray.main.ts +++ b/jslib/electron/src/tray.main.ts @@ -11,7 +11,8 @@ import { } from "electron"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; -import { StateService } from "@/jslib/common/src/abstractions/state.service"; + +import { StateService } from "@/src/abstractions/state.service"; import { WindowMain } from "./window.main"; diff --git a/jslib/electron/src/window.main.ts b/jslib/electron/src/window.main.ts index 83445a0e..acc84079 100644 --- a/jslib/electron/src/window.main.ts +++ b/jslib/electron/src/window.main.ts @@ -4,7 +4,8 @@ import * as url from "url"; import { app, BrowserWindow, Rectangle, screen } from "electron"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateService } from "@/jslib/common/src/abstractions/state.service"; + +import { StateService } from "@/src/abstractions/state.service"; import { cleanUserAgent, isDev, isMacAppStore, isSnapStore } from "./utils"; @@ -237,13 +238,15 @@ export class WindowMain { } private async getWindowState(defaultWidth: number, defaultHeight: number) { - const state = await this.stateService.getWindow(); + let state = await this.stateService.getWindow(); const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); let displayBounds: Rectangle = null; if (!isValid) { - state.width = defaultWidth; - state.height = defaultHeight; + state = { + width: defaultWidth, + height: defaultHeight, + }; displayBounds = screen.getPrimaryDisplay().bounds; } else if (this.stateHasBounds(state) && state.displayBounds) { diff --git a/jslib/node/src/cli/baseProgram.ts b/jslib/node/src/cli/baseProgram.ts index c69005b4..853a06f2 100644 --- a/jslib/node/src/cli/baseProgram.ts +++ b/jslib/node/src/cli/baseProgram.ts @@ -99,8 +99,11 @@ export abstract class BaseProgram { protected async exitIfAuthed() { const authed = await this.stateService.getIsAuthenticated(); if (authed) { - const email = await this.stateService.getEmail(); - this.processResponse(Response.error("You are already logged in as " + email + "."), true); + const organizationId = await this.stateService.getEntityId(); + this.processResponse( + Response.error("You are already logged in to" + organizationId + "."), + true, + ); } } diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts new file mode 100644 index 00000000..5271ee3a --- /dev/null +++ b/src/abstractions/environment.service.ts @@ -0,0 +1,13 @@ +import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; + +export { EnvironmentUrls }; + +export abstract class EnvironmentService { + abstract setUrls(urls: EnvironmentUrls): Promise; + abstract setUrlsFromStorage(): Promise; + + abstract hasBaseUrl(): boolean; + abstract getApiUrl(): string; + abstract getIdentityUrl(): string; + abstract getWebVaultUrl(): string; +} diff --git a/src/abstractions/state-vNext.service.ts b/src/abstractions/state-vNext.service.ts deleted file mode 100644 index c6afbb34..00000000 --- a/src/abstractions/state-vNext.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; -import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"; - -import { DirectoryType } from "@/src/enums/directoryType"; -import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration"; -import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; -import { LdapConfiguration } from "@/src/models/ldapConfiguration"; -import { OktaConfiguration } from "@/src/models/oktaConfiguration"; -import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration"; -import { SyncConfiguration } from "@/src/models/syncConfiguration"; - -export abstract class StateServiceVNext { - abstract getDirectory(type: DirectoryType): Promise; - abstract setDirectory( - type: DirectoryType, - config: - | LdapConfiguration - | GSuiteConfiguration - | EntraIdConfiguration - | OktaConfiguration - | OneLoginConfiguration, - ): Promise; - abstract getLdapConfiguration(options?: StorageOptions): Promise; - abstract setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise; - abstract getGsuiteConfiguration(options?: StorageOptions): Promise; - abstract setGsuiteConfiguration( - value: GSuiteConfiguration, - options?: StorageOptions, - ): Promise; - abstract getEntraConfiguration(options?: StorageOptions): Promise; - abstract setEntraConfiguration( - value: EntraIdConfiguration, - options?: StorageOptions, - ): Promise; - abstract getOktaConfiguration(options?: StorageOptions): Promise; - abstract setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise; - abstract getOneLoginConfiguration(options?: StorageOptions): Promise; - abstract setOneLoginConfiguration( - value: OneLoginConfiguration, - options?: StorageOptions, - ): Promise; - abstract getOrganizationId(options?: StorageOptions): Promise; - abstract setOrganizationId(value: string, options?: StorageOptions): Promise; - abstract getSync(options?: StorageOptions): Promise; - abstract setSync(value: SyncConfiguration, options?: StorageOptions): Promise; - abstract getDirectoryType(options?: StorageOptions): Promise; - abstract setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise; - abstract getUserDelta(options?: StorageOptions): Promise; - abstract setUserDelta(value: string, options?: StorageOptions): Promise; - abstract getLastUserSync(options?: StorageOptions): Promise; - abstract setLastUserSync(value: Date, options?: StorageOptions): Promise; - abstract getLastGroupSync(options?: StorageOptions): Promise; - abstract setLastGroupSync(value: Date, options?: StorageOptions): Promise; - abstract getGroupDelta(options?: StorageOptions): Promise; - abstract setGroupDelta(value: string, options?: StorageOptions): Promise; - abstract getLastSyncHash(options?: StorageOptions): Promise; - abstract setLastSyncHash(value: string, options?: StorageOptions): Promise; - abstract getSyncingDir(options?: StorageOptions): Promise; - abstract setSyncingDir(value: boolean, options?: StorageOptions): Promise; - abstract clearSyncSettings(syncHashToo: boolean): Promise; - - // Window settings (for WindowMain) - abstract getWindow(options?: StorageOptions): Promise; - abstract setWindow(value: any, options?: StorageOptions): Promise; - abstract getEnableAlwaysOnTop(options?: StorageOptions): Promise; - abstract setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise; - - // Tray settings (for TrayMain) - abstract getEnableTray(options?: StorageOptions): Promise; - abstract setEnableTray(value: boolean, options?: StorageOptions): Promise; - abstract getEnableMinimizeToTray(options?: StorageOptions): Promise; - abstract setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise; - abstract getEnableCloseToTray(options?: StorageOptions): Promise; - abstract setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise; - abstract getAlwaysShowDock(options?: StorageOptions): Promise; - abstract setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise; - - // Environment URLs (adding convenience methods) - abstract getEnvironmentUrls(options?: StorageOptions): Promise; - abstract setEnvironmentUrls(value: EnvironmentUrls): Promise; - abstract getApiUrl(options?: StorageOptions): Promise; - abstract getIdentityUrl(options?: StorageOptions): Promise; - - // Token management (replaces TokenService.clearToken()) - abstract clearAuthTokens(): Promise; -} diff --git a/src/abstractions/state.service.ts b/src/abstractions/state.service.ts index f6bb288e..c81a2722 100644 --- a/src/abstractions/state.service.ts +++ b/src/abstractions/state.service.ts @@ -1,8 +1,7 @@ -import { StateService as BaseStateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service"; +import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"; import { DirectoryType } from "@/src/enums/directoryType"; -import { Account } from "@/src/models/account"; import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration"; import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; import { LdapConfiguration } from "@/src/models/ldapConfiguration"; @@ -10,9 +9,9 @@ import { OktaConfiguration } from "@/src/models/oktaConfiguration"; import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration"; import { SyncConfiguration } from "@/src/models/syncConfiguration"; -export abstract class StateService extends BaseStateServiceAbstraction { - getDirectory: (type: DirectoryType) => Promise; - setDirectory: ( +export abstract class StateService { + abstract getDirectory(type: DirectoryType): Promise; + abstract setDirectory( type: DirectoryType, config: | LdapConfiguration @@ -20,37 +19,89 @@ export abstract class StateService extends BaseStateServiceAbstraction | EntraIdConfiguration | OktaConfiguration | OneLoginConfiguration, - ) => Promise; - getLdapConfiguration: (options?: StorageOptions) => Promise; - setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise; - getGsuiteConfiguration: (options?: StorageOptions) => Promise; - setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise; - getEntraConfiguration: (options?: StorageOptions) => Promise; - setEntraConfiguration: (value: EntraIdConfiguration, options?: StorageOptions) => Promise; - getOktaConfiguration: (options?: StorageOptions) => Promise; - setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise; - getOneLoginConfiguration: (options?: StorageOptions) => Promise; - setOneLoginConfiguration: ( + ): Promise; + abstract getLdapConfiguration(options?: StorageOptions): Promise; + abstract setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise; + abstract getGsuiteConfiguration(options?: StorageOptions): Promise; + abstract setGsuiteConfiguration( + value: GSuiteConfiguration, + options?: StorageOptions, + ): Promise; + abstract getEntraConfiguration(options?: StorageOptions): Promise; + abstract setEntraConfiguration( + value: EntraIdConfiguration, + options?: StorageOptions, + ): Promise; + abstract getOktaConfiguration(options?: StorageOptions): Promise; + abstract setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise; + abstract getOneLoginConfiguration(options?: StorageOptions): Promise; + abstract setOneLoginConfiguration( value: OneLoginConfiguration, options?: StorageOptions, - ) => Promise; - getOrganizationId: (options?: StorageOptions) => Promise; - setOrganizationId: (value: string, options?: StorageOptions) => Promise; - getSync: (options?: StorageOptions) => Promise; - setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise; - getDirectoryType: (options?: StorageOptions) => Promise; - setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise; - getUserDelta: (options?: StorageOptions) => Promise; - setUserDelta: (value: string, options?: StorageOptions) => Promise; - getLastUserSync: (options?: StorageOptions) => Promise; - setLastUserSync: (value: Date, options?: StorageOptions) => Promise; - getLastGroupSync: (options?: StorageOptions) => Promise; - setLastGroupSync: (value: Date, options?: StorageOptions) => Promise; - getGroupDelta: (options?: StorageOptions) => Promise; - setGroupDelta: (value: string, options?: StorageOptions) => Promise; - getLastSyncHash: (options?: StorageOptions) => Promise; - setLastSyncHash: (value: string, options?: StorageOptions) => Promise; - getSyncingDir: (options?: StorageOptions) => Promise; - setSyncingDir: (value: boolean, options?: StorageOptions) => Promise; - clearSyncSettings: (syncHashToo: boolean) => Promise; + ): Promise; + abstract getOrganizationId(options?: StorageOptions): Promise; + abstract setOrganizationId(value: string, options?: StorageOptions): Promise; + abstract getSync(options?: StorageOptions): Promise; + abstract setSync(value: SyncConfiguration, options?: StorageOptions): Promise; + abstract getDirectoryType(options?: StorageOptions): Promise; + abstract setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise; + abstract getUserDelta(options?: StorageOptions): Promise; + abstract setUserDelta(value: string, options?: StorageOptions): Promise; + abstract getLastUserSync(options?: StorageOptions): Promise; + abstract setLastUserSync(value: Date, options?: StorageOptions): Promise; + abstract getLastGroupSync(options?: StorageOptions): Promise; + abstract setLastGroupSync(value: Date, options?: StorageOptions): Promise; + abstract getGroupDelta(options?: StorageOptions): Promise; + abstract setGroupDelta(value: string, options?: StorageOptions): Promise; + abstract getLastSyncHash(options?: StorageOptions): Promise; + abstract setLastSyncHash(value: string, options?: StorageOptions): Promise; + abstract getSyncingDir(options?: StorageOptions): Promise; + abstract setSyncingDir(value: boolean, options?: StorageOptions): Promise; + abstract clearSyncSettings(syncHashToo: boolean): Promise; + + // Window settings (for WindowMain) + abstract getWindow(options?: StorageOptions): Promise; + abstract setWindow(value: any, options?: StorageOptions): Promise; + abstract getEnableAlwaysOnTop(options?: StorageOptions): Promise; + abstract setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise; + + // Tray settings (for TrayMain) + abstract getEnableTray(options?: StorageOptions): Promise; + abstract setEnableTray(value: boolean, options?: StorageOptions): Promise; + abstract getEnableMinimizeToTray(options?: StorageOptions): Promise; + abstract setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise; + abstract getEnableCloseToTray(options?: StorageOptions): Promise; + abstract setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise; + abstract getAlwaysShowDock(options?: StorageOptions): Promise; + abstract setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise; + + // Environment URLs (adding convenience methods) + abstract getEnvironmentUrls(options?: StorageOptions): Promise; + abstract setEnvironmentUrls(value: EnvironmentUrls): Promise; + abstract getApiUrl(options?: StorageOptions): Promise; + abstract getIdentityUrl(options?: StorageOptions): Promise; + + // Token management (replaces TokenService.clearToken()) + abstract clearAuthTokens(): Promise; + abstract getAccessToken(options?: StorageOptions): Promise; + abstract setAccessToken(value: string, options?: StorageOptions): Promise; + abstract getRefreshToken(options?: StorageOptions): Promise; + abstract setRefreshToken(value: string, options?: StorageOptions): Promise; + abstract getApiKeyClientId(options?: StorageOptions): Promise; + abstract setApiKeyClientId(value: string, options?: StorageOptions): Promise; + abstract getApiKeyClientSecret(options?: StorageOptions): Promise; + abstract setApiKeyClientSecret(value: string, options?: StorageOptions): Promise; + + // Lifecycle methods + abstract init(): Promise; + abstract clean(options?: StorageOptions): Promise; + + // Additional state methods + abstract getLocale(options?: StorageOptions): Promise; + abstract setLocale(value: string, options?: StorageOptions): Promise; + abstract getInstalledVersion(options?: StorageOptions): Promise; + abstract setInstalledVersion(value: string, options?: StorageOptions): Promise; + abstract getIsAuthenticated(options?: StorageOptions): Promise; + abstract getEntityId(options?: StorageOptions): Promise; + abstract setEntityId(value: string, options?: StorageOptions): Promise; } diff --git a/src/abstractions/token.service.ts b/src/abstractions/token.service.ts new file mode 100644 index 00000000..82880d73 --- /dev/null +++ b/src/abstractions/token.service.ts @@ -0,0 +1,25 @@ +import { DecodedToken } from "@/src/utils/jwt.util"; + +export abstract class TokenService { + // Token storage + abstract setTokens( + accessToken: string, + refreshToken: string, + clientIdClientSecret?: [string, string], + ): Promise; + abstract getToken(): Promise; + abstract getRefreshToken(): Promise; + abstract clearToken(): Promise; + + // API key authentication + abstract getClientId(): Promise; + abstract getClientSecret(): Promise; + + // Two-factor token (rarely used) + abstract getTwoFactorToken(): Promise; + abstract clearTwoFactorToken(): Promise; + + // Token validation (delegates to jwt.util) + abstract decodeToken(token?: string): Promise; + abstract tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise; +} diff --git a/src/app/accounts/environment.component.ts b/src/app/accounts/environment.component.ts index ad9edce0..ea5cbff4 100644 --- a/src/app/accounts/environment.component.ts +++ b/src/app/accounts/environment.component.ts @@ -1,21 +1,64 @@ -import { Component } from "@angular/core"; +import { Component, EventEmitter, OnInit, Output } from "@angular/core"; -import { EnvironmentComponent as BaseEnvironmentComponent } from "@/jslib/angular/src/components/environment.component"; -import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service"; +import { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service"; +import { StateService } from "@/src/abstractions/state.service"; + @Component({ selector: "app-environment", templateUrl: "environment.component.html", standalone: false, }) -export class EnvironmentComponent extends BaseEnvironmentComponent { +export class EnvironmentComponent implements OnInit { + @Output() onSaved = new EventEmitter(); + + identityUrl: string; + apiUrl: string; + webVaultUrl: string; + baseUrl: string; + showCustom = false; + constructor( - environmentService: EnvironmentService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - ) { - super(platformUtilsService, environmentService, i18nService); + private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private stateService: StateService, + ) {} + + async ngOnInit(): Promise { + // Load environment URLs from state + const urls = await this.stateService.getEnvironmentUrls(); + + this.baseUrl = urls?.base || ""; + this.webVaultUrl = urls?.webVault || ""; + this.apiUrl = urls?.api || ""; + this.identityUrl = urls?.identity || ""; + } + + async submit(): Promise { + const urls: EnvironmentUrls = { + base: this.baseUrl, + api: this.apiUrl, + identity: this.identityUrl, + webVault: this.webVaultUrl, + }; + + await this.environmentService.setUrls(urls); + + // Reload from state to get normalized URLs (with https:// prefix, etc.) + const normalizedUrls = await this.stateService.getEnvironmentUrls(); + this.baseUrl = normalizedUrls?.base || ""; + this.apiUrl = normalizedUrls?.api || ""; + this.identityUrl = normalizedUrls?.identity || ""; + this.webVaultUrl = normalizedUrls?.webVault || ""; + + this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved")); + this.onSaved.emit(); + } + + toggleCustom(): void { + this.showCustom = !this.showCustom; } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9df75612..f34d8970 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,7 +17,6 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service"; import { AuthService } from "../abstractions/auth.service"; -import { StateServiceVNext } from "../abstractions/state-vNext.service"; import { StateService } from "../abstractions/state.service"; import { SyncService } from "../services/sync.service"; @@ -35,7 +34,7 @@ export class AppComponent implements OnInit { constructor( private broadcasterService: BroadcasterService, - private stateServiceVNext: StateServiceVNext, + private stateService: StateService, private authService: AuthService, private router: Router, private toastrService: ToastrService, @@ -45,7 +44,6 @@ export class AppComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private syncService: SyncService, - private stateService: StateService, private logService: LogService, ) {} @@ -116,7 +114,7 @@ export class AppComponent implements OnInit { } private async logOut(expired: boolean) { - await this.stateServiceVNext.clearAuthTokens(); + await this.stateService.clearAuthTokens(); await this.stateService.clean(); this.authService.logOut(async () => { diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index a9a09d3a..6b150b1c 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -1,20 +1,24 @@ -import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { + APP_INITIALIZER, + ApplicationRef, + ComponentFactoryResolver, + Injector, + NgModule, +} from "@angular/core"; -import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module"; +import { BroadcasterService as BroadcasterServiceImplementation } from "@/jslib/angular/src/services/broadcaster.service"; +import { ModalService } from "@/jslib/angular/src/services/modal.service"; +import { ValidationService } from "@/jslib/angular/src/services/validation.service"; import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service"; import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service"; -import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service"; import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service"; -import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service"; import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service"; -import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service"; -import { StateFactory } from "@/jslib/common/src/factories/stateFactory"; -import { GlobalState } from "@/jslib/common/src/models/domain/globalState"; +import { AppIdService } from "@/jslib/common/src/services/appId.service"; import { ElectronLogService } from "@/jslib/electron/src/services/electronLog.service"; import { ElectronPlatformUtilsService } from "@/jslib/electron/src/services/electronPlatformUtils.service"; import { ElectronRendererMessagingService } from "@/jslib/electron/src/services/electronRendererMessaging.service"; @@ -24,32 +28,33 @@ import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service"; import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service"; import { DirectoryFactoryService } from "@/src/abstractions/directory-factory.service"; +import { EnvironmentService as EnvironmentServiceAbstraction } from "@/src/abstractions/environment.service"; +import { TokenService as TokenServiceAbstraction } from "@/src/abstractions/token.service"; import { BatchRequestBuilder } from "@/src/services/batch-request-builder"; import { DefaultDirectoryFactoryService } from "@/src/services/directory-factory.service"; import { SingleRequestBuilder } from "@/src/services/single-request-builder"; +import { StateMigrationService } from "@/src/services/state-service/stateMigration.service"; import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service"; -import { StateServiceVNext } from "../../abstractions/state-vNext.service"; import { StateService as StateServiceAbstraction } from "../../abstractions/state.service"; -import { Account } from "../../models/account"; import { AuthService } from "../../services/auth.service"; +import { EnvironmentService as EnvironmentServiceImplementation } from "../../services/environment/environment.service"; import { I18nService } from "../../services/i18n.service"; -import { StateServiceVNextImplementation } from "../../services/state-service/state-vNext.service"; -import { StateService } from "../../services/state-service/state.service"; -import { StateMigrationService } from "../../services/state-service/stateMigration.service"; +import { StateServiceImplementation } from "../../services/state-service/state.service"; import { SyncService } from "../../services/sync.service"; +import { TokenService as TokenServiceImplementation } from "../../services/token/token.service"; import { AuthGuardService } from "./auth-guard.service"; import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens"; import { LaunchGuardService } from "./launch-guard.service"; import { SafeProvider, safeProvider } from "./safe-provider"; -export function initFactory( - i18nService: I18nServiceAbstraction, - platformUtilsService: PlatformUtilsServiceAbstraction, - stateService: StateServiceAbstraction, -): () => Promise { +export function initFactory(injector: Injector): () => Promise { return async () => { + const stateService = injector.get(StateServiceAbstraction); + const i18nService = injector.get(I18nServiceAbstraction); + const platformUtilsService = injector.get(PlatformUtilsServiceAbstraction); + await stateService.init(); await (i18nService as I18nService).init(); const htmlEl = window.document.documentElement; @@ -73,21 +78,30 @@ export function initFactory( } @NgModule({ - imports: [JslibServicesModule], + imports: [], declarations: [], providers: [ safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => void>, useFactory: initFactory, - deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, StateServiceAbstraction], + deps: [Injector], multi: true, }), + safeProvider({ + provide: WINDOW, + useValue: window, + }), safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }), safeProvider({ provide: I18nServiceAbstraction, useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"), deps: [WINDOW], }), + safeProvider({ + provide: BroadcasterServiceAbstraction, + useClass: BroadcasterServiceImplementation, + deps: [], + }), safeProvider({ provide: MessagingServiceAbstraction, useClass: ElectronRendererMessagingService, @@ -117,6 +131,11 @@ export function initFactory( useClass: NodeCryptoFunctionService, deps: [], }), + safeProvider({ + provide: AppIdServiceAbstraction, + useClass: AppIdService, + deps: [StorageServiceAbstraction], + }), safeProvider({ provide: ApiServiceAbstraction, useFactory: ( @@ -165,7 +184,6 @@ export function initFactory( ApiServiceAbstraction, MessagingServiceAbstraction, I18nServiceAbstraction, - StateServiceVNext, StateServiceAbstraction, BatchRequestBuilder, SingleRequestBuilder, @@ -174,64 +192,50 @@ export function initFactory( }), safeProvider(AuthGuardService), safeProvider(LaunchGuardService), + // Provide StateMigrationService safeProvider({ - provide: StateMigrationServiceAbstraction, + provide: StateMigrationService, useFactory: ( storageService: StorageServiceAbstraction, secureStorageService: StorageServiceAbstraction, - ) => - new StateMigrationService( - storageService, - secureStorageService, - new StateFactory(GlobalState, Account), - ), + ) => new StateMigrationService(storageService, secureStorageService), deps: [StorageServiceAbstraction, SECURE_STORAGE], }), + // Use new StateService with flat key-value structure safeProvider({ provide: StateServiceAbstraction, useFactory: ( storageService: StorageServiceAbstraction, secureStorageService: StorageServiceAbstraction, logService: LogServiceAbstraction, - stateMigrationService: StateMigrationServiceAbstraction, + stateMigrationService: StateMigrationService, ) => - new StateService( + new StateServiceImplementation( storageService, secureStorageService, logService, stateMigrationService, true, - new StateFactory(GlobalState, Account), ), deps: [ StorageServiceAbstraction, SECURE_STORAGE, LogServiceAbstraction, - StateMigrationServiceAbstraction, + StateMigrationService, ], }), - // Use new StateServiceVNext with flat key-value structure (new interface) + // Provide TokenService and EnvironmentService safeProvider({ - provide: StateServiceVNext, - useFactory: ( - storageService: StorageServiceAbstraction, - secureStorageService: StorageServiceAbstraction, - logService: LogServiceAbstraction, - stateMigrationService: StateMigrationServiceAbstraction, - ) => - new StateServiceVNextImplementation( - storageService, - secureStorageService, - logService, - stateMigrationService, - true, - ), - deps: [ - StorageServiceAbstraction, - SECURE_STORAGE, - LogServiceAbstraction, - StateMigrationServiceAbstraction, - ], + provide: TokenServiceAbstraction, + useFactory: (secureStorage: StorageServiceAbstraction) => + new TokenServiceImplementation(secureStorage), + deps: [SECURE_STORAGE], + }), + safeProvider({ + provide: EnvironmentServiceAbstraction, + useFactory: (stateService: StateServiceAbstraction) => + new EnvironmentServiceImplementation(stateService), + deps: [StateServiceAbstraction], }), safeProvider({ provide: SingleRequestBuilder, @@ -244,12 +248,17 @@ export function initFactory( safeProvider({ provide: DirectoryFactoryService, useClass: DefaultDirectoryFactoryService, - deps: [ - LogServiceAbstraction, - I18nServiceAbstraction, - StateServiceAbstraction, - StateServiceVNext, - ], + deps: [LogServiceAbstraction, I18nServiceAbstraction, StateServiceAbstraction], + }), + safeProvider({ + provide: ModalService, + useClass: ModalService, + deps: [ComponentFactoryResolver, ApplicationRef, Injector], + }), + safeProvider({ + provide: ValidationService, + useClass: ValidationService, + deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], }), ] satisfies SafeProvider[], }) diff --git a/src/bwdc.ts b/src/bwdc.ts index a631c1c6..2615d8f0 100644 --- a/src/bwdc.ts +++ b/src/bwdc.ts @@ -4,32 +4,30 @@ import * as path from "path"; import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service"; import { ClientType } from "@/jslib/common/src/enums/clientType"; import { LogLevelType } from "@/jslib/common/src/enums/logLevelType"; -import { StateFactory } from "@/jslib/common/src/factories/stateFactory"; -import { GlobalState } from "@/jslib/common/src/models/domain/globalState"; import { AppIdService } from "@/jslib/common/src/services/appId.service"; -import { EnvironmentService } from "@/jslib/common/src/services/environment.service"; import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service"; -import { TokenService } from "@/jslib/common/src/services/token.service"; import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service"; import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service"; import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service"; import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service"; import { DirectoryFactoryService } from "./abstractions/directory-factory.service"; -import { StateServiceVNext } from "./abstractions/state-vNext.service"; -import { Account } from "./models/account"; +import { EnvironmentService } from "./abstractions/environment.service"; +import { StateService } from "./abstractions/state.service"; +import { TokenService } from "./abstractions/token.service"; import { Program } from "./program"; import { AuthService } from "./services/auth.service"; import { BatchRequestBuilder } from "./services/batch-request-builder"; import { DefaultDirectoryFactoryService } from "./services/directory-factory.service"; +import { EnvironmentService as EnvironmentServiceImplementation } from "./services/environment/environment.service"; import { I18nService } from "./services/i18n.service"; import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service"; import { LowdbStorageService } from "./services/lowdbStorage.service"; import { SingleRequestBuilder } from "./services/single-request-builder"; -import { StateServiceVNextImplementation } from "./services/state-service/state-vNext.service"; -import { StateService } from "./services/state-service/state.service"; +import { StateServiceImplementation } from "./services/state-service/state.service"; import { StateMigrationService } from "./services/state-service/stateMigration.service"; import { SyncService } from "./services/sync.service"; +import { TokenService as TokenServiceImplementation } from "./services/token/token.service"; // eslint-disable-next-line const packageJson = require("../package.json"); @@ -51,7 +49,6 @@ export class Main { cryptoFunctionService: NodeCryptoFunctionService; authService: AuthService; syncService: SyncService; - stateServiceVNext: StateServiceVNext; stateService: StateService; stateMigrationService: StateMigrationService; directoryFactoryService: DirectoryFactoryService; @@ -104,19 +101,10 @@ export class Main { this.stateMigrationService = new StateMigrationService( this.storageService, this.secureStorageService, - new StateFactory(GlobalState, Account), ); - this.stateService = new StateService( - this.storageService, - this.secureStorageService, - this.logService, - this.stateMigrationService, - process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true", - new StateFactory(GlobalState, Account), - ); - // Use new StateServiceVNext with flat key-value structure - this.stateServiceVNext = new StateServiceVNextImplementation( + // Use new StateService with flat key-value structure + this.stateService = new StateServiceImplementation( this.storageService, this.secureStorageService, this.logService, @@ -125,9 +113,9 @@ export class Main { ); this.appIdService = new AppIdService(this.storageService); - this.tokenService = new TokenService(this.stateService); + this.tokenService = new TokenServiceImplementation(this.secureStorageService); this.messagingService = new NoopMessagingService(); - this.environmentService = new EnvironmentService(this.stateService); + this.environmentService = new EnvironmentServiceImplementation(this.stateService); const customUserAgent = "Bitwarden_DC/" + @@ -156,7 +144,6 @@ export class Main { this.logService, this.i18nService, this.stateService, - this.stateServiceVNext, ); this.batchRequestBuilder = new BatchRequestBuilder(); @@ -167,7 +154,6 @@ export class Main { this.apiService, this.messagingService, this.i18nService, - this.stateServiceVNext, this.stateService, this.batchRequestBuilder, this.singleRequestBuilder, @@ -183,7 +169,7 @@ export class Main { } async logout() { - await this.stateServiceVNext.clearAuthTokens(); + await this.stateService.clearAuthTokens(); await this.stateService.clean(); } diff --git a/src/commands/config.command.ts b/src/commands/config.command.ts index a7b0f4c6..b3aa91e9 100644 --- a/src/commands/config.command.ts +++ b/src/commands/config.command.ts @@ -6,7 +6,6 @@ import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrl import { Response } from "@/jslib/node/src/cli/models/response"; import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse"; -import { StateServiceVNext } from "../abstractions/state-vNext.service"; import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; import { EntraIdConfiguration } from "../models/entraIdConfiguration"; @@ -27,9 +26,8 @@ export class ConfigCommand { private sync = new SyncConfiguration(); constructor( - private stateServiceVNext: StateServiceVNext, - private i18nService: I18nService, private stateService: StateService, + private i18nService: I18nService, ) {} async run(setting: string, value: string, options: program.OptionValues): Promise { @@ -81,7 +79,7 @@ export class ConfigCommand { url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url; const urls = new EnvironmentUrls(); urls.base = url; - await this.stateServiceVNext.setEnvironmentUrls(urls); + await this.stateService.setEnvironmentUrls(urls); } private async setDirectory(type: string) { diff --git a/src/main.ts b/src/main.ts index 9b4c96de..cc3f9b8b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,8 +2,6 @@ import * as path from "path"; import { app } from "electron"; -import { StateFactory } from "@/jslib/common/src/factories/stateFactory"; -import { GlobalState } from "@/jslib/common/src/models/domain/globalState"; import { ElectronLogService } from "@/jslib/electron/src/services/electronLog.service"; import { ElectronMainMessagingService } from "@/jslib/electron/src/services/electronMainMessaging.service"; import { ElectronStorageService } from "@/jslib/electron/src/services/electronStorage.service"; @@ -11,14 +9,12 @@ import { TrayMain } from "@/jslib/electron/src/tray.main"; import { UpdaterMain } from "@/jslib/electron/src/updater.main"; import { WindowMain } from "@/jslib/electron/src/window.main"; -import { StateServiceVNext } from "./abstractions/state-vNext.service"; +import { StateService } from "./abstractions/state.service"; import { DCCredentialStorageListener } from "./main/credential-storage-listener"; import { MenuMain } from "./main/menu.main"; import { MessagingMain } from "./main/messaging.main"; -import { Account } from "./models/account"; import { I18nService } from "./services/i18n.service"; -import { StateServiceVNextImplementation } from "./services/state-service/state-vNext.service"; -import { StateService } from "./services/state-service/state.service"; +import { StateServiceImplementation } from "./services/state-service/state.service"; export class Main { logService: ElectronLogService; @@ -26,7 +22,6 @@ export class Main { storageService: ElectronStorageService; messagingService: ElectronMainMessagingService; credentialStorageListener: DCCredentialStorageListener; - stateServiceVNext: StateServiceVNext; stateService: StateService; windowMain: WindowMain; @@ -61,16 +56,8 @@ export class Main { this.logService.init(); this.i18nService = new I18nService("en", "./locales/"); this.storageService = new ElectronStorageService(app.getPath("userData")); - this.stateService = new StateService( - this.storageService, - null, - this.logService, - null, - true, - new StateFactory(GlobalState, Account), - ); - // Use new StateServiceVNext with flat key-value structure - this.stateServiceVNext = new StateServiceVNextImplementation( + // Use new StateService with flat key-value structure + this.stateService = new StateServiceImplementation( this.storageService, null, this.logService, @@ -79,7 +66,7 @@ export class Main { ); this.windowMain = new WindowMain( - this.stateServiceVNext, + this.stateService, this.logService, false, 800, @@ -105,7 +92,7 @@ export class Main { "bitwardenDirectoryConnector", ); - this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateServiceVNext); + this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService); this.messagingMain = new MessagingMain( this.windowMain, diff --git a/src/models/account.ts b/src/models/account.ts index 4cc644d7..93f9bfb3 100644 --- a/src/models/account.ts +++ b/src/models/account.ts @@ -1,5 +1,3 @@ -import { Account as BaseAccount } from "@/jslib/common/src/models/domain/account"; - import { DirectoryType } from "@/src/enums/directoryType"; import { EntraIdConfiguration } from "./entraIdConfiguration"; @@ -9,18 +7,6 @@ import { OktaConfiguration } from "./oktaConfiguration"; import { OneLoginConfiguration } from "./oneLoginConfiguration"; import { SyncConfiguration } from "./syncConfiguration"; -export class Account extends BaseAccount { - directoryConfigurations?: DirectoryConfigurations = new DirectoryConfigurations(); - directorySettings: DirectorySettings = new DirectorySettings(); - clientKeys: ClientKeys = new ClientKeys(); - - constructor(init: Partial) { - super(init); - this.directoryConfigurations = init?.directoryConfigurations ?? new DirectoryConfigurations(); - this.directorySettings = init?.directorySettings ?? new DirectorySettings(); - } -} - export class ClientKeys { clientId: string; clientSecret: string; diff --git a/src/program.ts b/src/program.ts index d62ccd68..ace68e01 100644 --- a/src/program.ts +++ b/src/program.ts @@ -209,11 +209,7 @@ export class Program extends BaseProgram { writeLn("", true); }) .action(async (setting: string, value: string, options: OptionValues) => { - const command = new ConfigCommand( - this.main.stateServiceVNext, - this.main.i18nService, - this.main.stateService, - ); + const command = new ConfigCommand(this.main.stateService, this.main.i18nService); const response = await command.run(setting, value, options); this.processResponse(response); }); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 645c6311..08ca791a 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -2,18 +2,12 @@ import { ApiService } from "@/jslib/common/src/abstractions/api.service"; import { AppIdService } from "@/jslib/common/src/abstractions/appId.service"; import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service"; import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service"; -import { - AccountKeys, - AccountProfile, - AccountTokens, -} from "@/jslib/common/src/models/domain/account"; import { DeviceRequest } from "@/jslib/common/src/models/request/deviceRequest"; import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken/apiTokenRequest"; import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor"; import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse"; import { StateService } from "../abstractions/state.service"; -import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account"; export class AuthService { constructor( @@ -60,32 +54,10 @@ export class AuthService { const entityId = clientId.split("organization.")[1]; const clientSecret = tokenRequest.clientSecret; - await this.stateService.addAccount( - new Account({ - profile: { - ...new AccountProfile(), - ...{ - userId: entityId, - apiKeyClientId: clientId, - entityId: entityId, - }, - }, - tokens: { - ...new AccountTokens(), - ...{ - accessToken: tokenResponse.accessToken, - refreshToken: tokenResponse.refreshToken, - }, - }, - keys: { - ...new AccountKeys(), - ...{ - apiKeyClientSecret: clientSecret, - }, - }, - directorySettings: new DirectorySettings(), - directoryConfigurations: new DirectoryConfigurations(), - }), - ); + await this.stateService.setAccessToken(tokenResponse.accessToken); + await this.stateService.setRefreshToken(tokenResponse.refreshToken); + await this.stateService.setApiKeyClientId(clientId); + await this.stateService.setApiKeyClientSecret(clientSecret); + await this.stateService.setEntityId(entityId); } } diff --git a/src/services/authService.spec.ts b/src/services/authService.spec.ts index 97ecb2ac..50ed0ce6 100644 --- a/src/services/authService.spec.ts +++ b/src/services/authService.spec.ts @@ -4,15 +4,9 @@ import { ApiService } from "@/jslib/common/src/abstractions/api.service"; import { AppIdService } from "@/jslib/common/src/abstractions/appId.service"; import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service"; import { Utils } from "@/jslib/common/src/misc/utils"; -import { - AccountKeys, - AccountProfile, - AccountTokens, -} from "@/jslib/common/src/models/domain/account"; import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse"; import { MessagingService } from "../../jslib/common/src/abstractions/messaging.service"; -import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account"; import { AuthService } from "./auth.service"; import { StateService } from "./state-service/state.service"; @@ -66,32 +60,15 @@ describe("AuthService", () => { await authService.logIn({ clientId, clientSecret }); - stateService.received(1).addAccount( - new Account({ - profile: { - ...new AccountProfile(), - ...{ - userId: "CLIENT_ID", - apiKeyClientId: clientId, // with the "organization." prefix - entityId: "CLIENT_ID", - }, - }, - tokens: { - ...new AccountTokens(), - ...{ - accessToken: accessToken, - refreshToken: refreshToken, - }, - }, - keys: { - ...new AccountKeys(), - ...{ - apiKeyClientSecret: clientSecret, - }, - }, - directorySettings: new DirectorySettings(), - directoryConfigurations: new DirectoryConfigurations(), - }), - ); + // Verify authentication tokens are saved + stateService.received(1).setAccessToken(accessToken); + stateService.received(1).setRefreshToken(refreshToken); + + // Verify API key credentials are saved + stateService.received(1).setApiKeyClientId(clientId); + stateService.received(1).setApiKeyClientSecret(clientSecret); + + // Verify entity ID is saved + stateService.received(1).setEntityId("CLIENT_ID"); }); }); diff --git a/src/services/directory-factory.service.ts b/src/services/directory-factory.service.ts index 0d1e80a4..eccea5c2 100644 --- a/src/services/directory-factory.service.ts +++ b/src/services/directory-factory.service.ts @@ -2,7 +2,6 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; import { DirectoryFactoryService } from "../abstractions/directory-factory.service"; -import { StateServiceVNext } from "../abstractions/state-vNext.service"; import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; @@ -17,18 +16,12 @@ export class DefaultDirectoryFactoryService implements DirectoryFactoryService { private logService: LogService, private i18nService: I18nService, private stateService: StateService, - private stateServiceVNext: StateServiceVNext, ) {} createService(directoryType: DirectoryType) { switch (directoryType) { case DirectoryType.GSuite: - return new GSuiteDirectoryService( - this.logService, - this.i18nService, - this.stateService, - this.stateServiceVNext, - ); + return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService); case DirectoryType.EntraID: return new EntraIdDirectoryService(this.logService, this.i18nService, this.stateService); case DirectoryType.Ldap: diff --git a/src/services/directory-services/gsuite-directory.service.integration.spec.ts b/src/services/directory-services/gsuite-directory.service.integration.spec.ts index 07521a19..77ad82cc 100644 --- a/src/services/directory-services/gsuite-directory.service.integration.spec.ts +++ b/src/services/directory-services/gsuite-directory.service.integration.spec.ts @@ -1,7 +1,7 @@ import { config as dotenvConfig } from "dotenv"; import { mock, MockProxy } from "jest-mock-extended"; -import { StateServiceVNext } from "@/src/abstractions/state-vNext.service"; +import { StateService } from "@/src/abstractions/state.service"; import { I18nService } from "../../../jslib/common/src/abstractions/i18n.service"; import { LogService } from "../../../jslib/common/src/abstractions/log.service"; @@ -12,7 +12,6 @@ import { import { groupFixtures } from "../../../utils/google-workspace/group-fixtures"; import { userFixtures } from "../../../utils/google-workspace/user-fixtures"; import { DirectoryType } from "../../enums/directoryType"; -import { StateService } from "../state-service/state.service"; import { GSuiteDirectoryService } from "./gsuite-directory.service"; @@ -37,7 +36,6 @@ describe("gsuiteDirectoryService", () => { let logService: MockProxy; let i18nService: MockProxy; let stateService: MockProxy; - let stateServiceVNext: MockProxy; let directoryService: GSuiteDirectoryService; @@ -45,31 +43,23 @@ describe("gsuiteDirectoryService", () => { logService = mock(); i18nService = mock(); stateService = mock(); - stateServiceVNext = mock(); - stateServiceVNext.getDirectoryType.mockResolvedValue(DirectoryType.GSuite); + stateService.getDirectoryType.mockResolvedValue(DirectoryType.GSuite); stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date i18nService.t.mockImplementation((id) => id); // passthrough implementation for any error messages - directoryService = new GSuiteDirectoryService( - logService, - i18nService, - stateService, - stateServiceVNext, - ); + directoryService = new GSuiteDirectoryService(logService, i18nService, stateService); }); it("syncs without using filters (includes test data)", async () => { const directoryConfig = getGSuiteConfiguration(); - stateServiceVNext.getDirectory - .calledWith(DirectoryType.GSuite) - .mockResolvedValue(directoryConfig); + stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig); const syncConfig = getSyncConfiguration({ groups: true, users: true, }); - stateServiceVNext.getSync.mockResolvedValue(syncConfig); + stateService.getSync.mockResolvedValue(syncConfig); const result = await directoryService.getEntries(true, true); @@ -79,9 +69,7 @@ describe("gsuiteDirectoryService", () => { it("syncs using user and group filters (exact match for test data)", async () => { const directoryConfig = getGSuiteConfiguration(); - stateServiceVNext.getDirectory - .calledWith(DirectoryType.GSuite) - .mockResolvedValue(directoryConfig); + stateService.getDirectory.calledWith(DirectoryType.GSuite).mockResolvedValue(directoryConfig); const syncConfig = getSyncConfiguration({ groups: true, @@ -89,7 +77,7 @@ describe("gsuiteDirectoryService", () => { userFilter: INTEGRATION_USER_FILTER, groupFilter: INTEGRATION_GROUP_FILTER, }); - stateServiceVNext.getSync.mockResolvedValue(syncConfig); + stateService.getSync.mockResolvedValue(syncConfig); const result = await directoryService.getEntries(true, true); diff --git a/src/services/directory-services/gsuite-directory.service.ts b/src/services/directory-services/gsuite-directory.service.ts index 9e710e48..5ae98629 100644 --- a/src/services/directory-services/gsuite-directory.service.ts +++ b/src/services/directory-services/gsuite-directory.service.ts @@ -4,9 +4,8 @@ import { admin_directory_v1, google } from "googleapis"; import { I18nService } from "@/jslib/common/src/abstractions/i18n.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateServiceVNext } from "@/src/abstractions/state-vNext.service"; +import { StateService } from "@/src/abstractions/state.service"; -import { StateService } from "../../abstractions/state.service"; import { DirectoryType } from "../../enums/directoryType"; import { GroupEntry } from "../../models/groupEntry"; import { GSuiteConfiguration } from "../../models/gsuiteConfiguration"; @@ -27,26 +26,25 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir private logService: LogService, private i18nService: I18nService, private stateService: StateService, - private stateServiceVNext: StateServiceVNext, ) { super(); this.service = google.admin("directory_v1"); } async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { - const type = await this.stateServiceVNext.getDirectoryType(); + const type = await this.stateService.getDirectoryType(); if (type !== DirectoryType.GSuite) { return; } - this.dirConfig = await this.stateServiceVNext.getDirectory( + this.dirConfig = await this.stateService.getDirectory( DirectoryType.GSuite, ); if (this.dirConfig == null) { return; } - this.syncConfig = await this.stateServiceVNext.getSync(); + this.syncConfig = await this.stateService.getSync(); if (this.syncConfig == null) { return; } diff --git a/src/services/environment/environment.service.ts b/src/services/environment/environment.service.ts new file mode 100644 index 00000000..ddd3b992 --- /dev/null +++ b/src/services/environment/environment.service.ts @@ -0,0 +1,78 @@ +import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; + +import { EnvironmentService as IEnvironmentService } from "@/src/abstractions/environment.service"; +import { StateService } from "@/src/abstractions/state.service"; + +export class EnvironmentService implements IEnvironmentService { + private readonly DEFAULT_URLS = { + api: "https://api.bitwarden.com", + identity: "https://identity.bitwarden.com", + webVault: "https://vault.bitwarden.com", + }; + + private urls: EnvironmentUrls = new EnvironmentUrls(); + + constructor(private stateService: StateService) {} + + async setUrls(urls: EnvironmentUrls): Promise { + // Normalize URLs: trim whitespace, remove trailing slashes, add https:// if missing + const normalized = new EnvironmentUrls(); + + for (const [key, value] of Object.entries(urls)) { + if (!value || typeof value !== "string") { + continue; + } + + let url = value.trim(); + url = url.replace(/\/+$/, ""); // Remove trailing slashes + + if (!/^https?:\/\//i.test(url)) { + url = `https://${url}`; + } + + normalized[key as keyof EnvironmentUrls] = url; + } + + this.urls = normalized; + await this.stateService.setEnvironmentUrls(normalized); + } + + async setUrlsFromStorage(): Promise { + const stored = await this.stateService.getEnvironmentUrls(); + this.urls = stored ?? new EnvironmentUrls(); + } + + hasBaseUrl(): boolean { + return !!this.urls.base; + } + + getApiUrl(): string { + if (this.urls.api) { + return this.urls.api; + } + if (this.urls.base) { + return this.urls.base + "/api"; + } + return this.DEFAULT_URLS.api; + } + + getIdentityUrl(): string { + if (this.urls.identity) { + return this.urls.identity; + } + if (this.urls.base) { + return this.urls.base + "/identity"; + } + return this.DEFAULT_URLS.identity; + } + + getWebVaultUrl(): string { + if (this.urls.webVault) { + return this.urls.webVault; + } + if (this.urls.base) { + return this.urls.base; + } + return this.DEFAULT_URLS.webVault; + } +} diff --git a/src/services/state-service/state-vNext.service.ts b/src/services/state-service/state-vNext.service.ts deleted file mode 100644 index ee56b439..00000000 --- a/src/services/state-service/state-vNext.service.ts +++ /dev/null @@ -1,499 +0,0 @@ -import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateMigrationService } from "@/jslib/common/src/abstractions/stateMigration.service"; -import { StorageService } from "@/jslib/common/src/abstractions/storage.service"; -import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; -import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"; - -import { StateServiceVNext as StateServiceVNextAbstraction } from "@/src/abstractions/state-vNext.service"; -import { DirectoryType } from "@/src/enums/directoryType"; -import { IConfiguration } from "@/src/models/IConfiguration"; -import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration"; -import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; -import { LdapConfiguration } from "@/src/models/ldapConfiguration"; -import { OktaConfiguration } from "@/src/models/oktaConfiguration"; -import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration"; -import { - SecureStorageKeysVNext as SecureStorageKeys, - StorageKeysVNext as StorageKeys, - StoredSecurely, -} from "@/src/models/state.model"; -import { SyncConfiguration } from "@/src/models/syncConfiguration"; - -export class StateServiceVNextImplementation implements StateServiceVNextAbstraction { - constructor( - protected storageService: StorageService, - protected secureStorageService: StorageService, - protected logService: LogService, - protected stateMigrationService: StateMigrationService, - private useSecureStorageForSecrets = true, - ) {} - - async init(): Promise { - if (await this.stateMigrationService.needsMigration()) { - await this.stateMigrationService.migrate(); - } - } - - async clean(options?: StorageOptions): Promise { - // Clear all directory settings and configurations - // but preserve version and environment settings - await this.setDirectoryType(null); - await this.setOrganizationId(null); - await this.setSync(null); - await this.setLdapConfiguration(null); - await this.setGsuiteConfiguration(null); - await this.setEntraConfiguration(null); - await this.setOktaConfiguration(null); - await this.setOneLoginConfiguration(null); - await this.clearSyncSettings(true); - } - - // =================================================================== - // Directory Configuration Methods - // =================================================================== - - async getDirectory(type: DirectoryType): Promise { - const config = await this.getConfiguration(type); - if (config == null) { - return config as T; - } - - if (this.useSecureStorageForSecrets) { - // Create a copy to avoid modifying the cached config - const configWithSecrets = Object.assign({}, config); - - switch (type) { - case DirectoryType.Ldap: - (configWithSecrets as any).password = await this.getLdapSecret(); - break; - case DirectoryType.EntraID: - (configWithSecrets as any).key = await this.getEntraSecret(); - break; - case DirectoryType.Okta: - (configWithSecrets as any).token = await this.getOktaSecret(); - break; - case DirectoryType.GSuite: - (configWithSecrets as any).privateKey = await this.getGsuiteSecret(); - break; - case DirectoryType.OneLogin: - (configWithSecrets as any).clientSecret = await this.getOneLoginSecret(); - break; - } - - return configWithSecrets as T; - } - - return config as T; - } - - async setDirectory( - type: DirectoryType, - config: - | LdapConfiguration - | GSuiteConfiguration - | EntraIdConfiguration - | OktaConfiguration - | OneLoginConfiguration, - ): Promise { - if (this.useSecureStorageForSecrets) { - switch (type) { - case DirectoryType.Ldap: { - const ldapConfig = config as LdapConfiguration; - await this.setLdapSecret(ldapConfig.password); - ldapConfig.password = StoredSecurely; - await this.setLdapConfiguration(ldapConfig); - break; - } - case DirectoryType.EntraID: { - const entraConfig = config as EntraIdConfiguration; - await this.setEntraSecret(entraConfig.key); - entraConfig.key = StoredSecurely; - await this.setEntraConfiguration(entraConfig); - break; - } - case DirectoryType.Okta: { - const oktaConfig = config as OktaConfiguration; - await this.setOktaSecret(oktaConfig.token); - oktaConfig.token = StoredSecurely; - await this.setOktaConfiguration(oktaConfig); - break; - } - case DirectoryType.GSuite: { - const gsuiteConfig = config as GSuiteConfiguration; - if (gsuiteConfig.privateKey == null) { - await this.setGsuiteSecret(null); - } else { - const normalizedPrivateKey = gsuiteConfig.privateKey.replace(/\\n/g, "\n"); - await this.setGsuiteSecret(normalizedPrivateKey); - gsuiteConfig.privateKey = StoredSecurely; - } - await this.setGsuiteConfiguration(gsuiteConfig); - break; - } - case DirectoryType.OneLogin: { - const oneLoginConfig = config as OneLoginConfiguration; - await this.setOneLoginSecret(oneLoginConfig.clientSecret); - oneLoginConfig.clientSecret = StoredSecurely; - await this.setOneLoginConfiguration(oneLoginConfig); - break; - } - } - } - } - - async getConfiguration(type: DirectoryType): Promise { - switch (type) { - case DirectoryType.Ldap: - return await this.getLdapConfiguration(); - case DirectoryType.GSuite: - return await this.getGsuiteConfiguration(); - case DirectoryType.EntraID: - return await this.getEntraConfiguration(); - case DirectoryType.Okta: - return await this.getOktaConfiguration(); - case DirectoryType.OneLogin: - return await this.getOneLoginConfiguration(); - } - } - - // =================================================================== - // Secret Storage Methods (Secure Storage) - // =================================================================== - - private async getLdapSecret(): Promise { - return await this.secureStorageService.get(SecureStorageKeys.ldap); - } - - private async setLdapSecret(value: string): Promise { - if (value == null) { - await this.secureStorageService.remove(SecureStorageKeys.ldap); - } else { - await this.secureStorageService.save(SecureStorageKeys.ldap, value); - } - } - - private async getGsuiteSecret(): Promise { - return await this.secureStorageService.get(SecureStorageKeys.gsuite); - } - - private async setGsuiteSecret(value: string): Promise { - if (value == null) { - await this.secureStorageService.remove(SecureStorageKeys.gsuite); - } else { - await this.secureStorageService.save(SecureStorageKeys.gsuite, value); - } - } - - private async getEntraSecret(): Promise { - // Try new key first, fall back to old azure key for backwards compatibility - const entraKey = await this.secureStorageService.get(SecureStorageKeys.entra); - if (entraKey != null) { - return entraKey; - } - return await this.secureStorageService.get(SecureStorageKeys.azure); - } - - private async setEntraSecret(value: string): Promise { - if (value == null) { - await this.secureStorageService.remove(SecureStorageKeys.entra); - await this.secureStorageService.remove(SecureStorageKeys.azure); - } else { - await this.secureStorageService.save(SecureStorageKeys.entra, value); - } - } - - private async getOktaSecret(): Promise { - return await this.secureStorageService.get(SecureStorageKeys.okta); - } - - private async setOktaSecret(value: string): Promise { - if (value == null) { - await this.secureStorageService.remove(SecureStorageKeys.okta); - } else { - await this.secureStorageService.save(SecureStorageKeys.okta, value); - } - } - - private async getOneLoginSecret(): Promise { - return await this.secureStorageService.get(SecureStorageKeys.oneLogin); - } - - private async setOneLoginSecret(value: string): Promise { - if (value == null) { - await this.secureStorageService.remove(SecureStorageKeys.oneLogin); - } else { - await this.secureStorageService.save(SecureStorageKeys.oneLogin, value); - } - } - - // =================================================================== - // Directory-Specific Configuration Methods - // =================================================================== - - async getLdapConfiguration(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.directory_ldap); - } - - async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.directory_ldap, value); - } - - async getGsuiteConfiguration(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.directory_gsuite); - } - - async setGsuiteConfiguration( - value: GSuiteConfiguration, - options?: StorageOptions, - ): Promise { - await this.storageService.save(StorageKeys.directory_gsuite, value); - } - - async getEntraConfiguration(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.directory_entra); - } - - async setEntraConfiguration( - value: EntraIdConfiguration, - options?: StorageOptions, - ): Promise { - await this.storageService.save(StorageKeys.directory_entra, value); - } - - async getOktaConfiguration(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.directory_okta); - } - - async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.directory_okta, value); - } - - async getOneLoginConfiguration(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.directory_onelogin); - } - - async setOneLoginConfiguration( - value: OneLoginConfiguration, - options?: StorageOptions, - ): Promise { - await this.storageService.save(StorageKeys.directory_onelogin, value); - } - - // =================================================================== - // Directory Settings Methods - // =================================================================== - - async getOrganizationId(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.organizationId); - } - - async setOrganizationId(value: string, options?: StorageOptions): Promise { - const currentId = await this.getOrganizationId(); - if (currentId !== value) { - await this.clearSyncSettings(); - } - await this.storageService.save(StorageKeys.organizationId, value); - } - - async getSync(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.sync); - } - - async setSync(value: SyncConfiguration, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.sync, value); - } - - async getDirectoryType(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.directoryType); - } - - async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise { - const currentType = await this.getDirectoryType(); - if (value !== currentType) { - await this.clearSyncSettings(); - } - await this.storageService.save(StorageKeys.directoryType, value); - } - - async getLastUserSync(options?: StorageOptions): Promise { - const dateString = await this.storageService.get(SecureStorageKeys.lastUserSync); - return dateString ? new Date(dateString) : null; - } - - async setLastUserSync(value: Date, options?: StorageOptions): Promise { - await this.storageService.save(SecureStorageKeys.lastUserSync, value); - } - - async getLastGroupSync(options?: StorageOptions): Promise { - const dateString = await this.storageService.get(SecureStorageKeys.lastGroupSync); - return dateString ? new Date(dateString) : null; - } - - async setLastGroupSync(value: Date, options?: StorageOptions): Promise { - await this.storageService.save(SecureStorageKeys.lastGroupSync, value); - } - - async getLastSyncHash(options?: StorageOptions): Promise { - return await this.storageService.get(SecureStorageKeys.lastSyncHash); - } - - async setLastSyncHash(value: string, options?: StorageOptions): Promise { - await this.storageService.save(SecureStorageKeys.lastSyncHash, value); - } - - async getSyncingDir(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.syncingDir); - } - - async setSyncingDir(value: boolean, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.syncingDir, value); - } - - async getUserDelta(options?: StorageOptions): Promise { - return await this.storageService.get(SecureStorageKeys.userDelta); - } - - async setUserDelta(value: string, options?: StorageOptions): Promise { - await this.storageService.save(SecureStorageKeys.userDelta, value); - } - - async getGroupDelta(options?: StorageOptions): Promise { - return await this.storageService.get(SecureStorageKeys.groupDelta); - } - - async setGroupDelta(value: string, options?: StorageOptions): Promise { - await this.storageService.save(SecureStorageKeys.groupDelta, value); - } - - async clearSyncSettings(hashToo = false): Promise { - await this.setUserDelta(null); - await this.setGroupDelta(null); - await this.setLastGroupSync(null); - await this.setLastUserSync(null); - if (hashToo) { - await this.setLastSyncHash(null); - } - } - - // =================================================================== - // Environment URLs - // =================================================================== - - async getEnvironmentUrls(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.environmentUrls); - } - - async setEnvironmentUrls(value: EnvironmentUrls): Promise { - await this.storageService.save(StorageKeys.environmentUrls, value); - } - - async getApiUrl(options?: StorageOptions): Promise { - const urls = await this.getEnvironmentUrls(options); - if (urls?.api) { - return urls.api; - } - if (urls?.base) { - return urls.base + "/api"; - } - return "https://api.bitwarden.com"; - } - - async getIdentityUrl(options?: StorageOptions): Promise { - const urls = await this.getEnvironmentUrls(options); - if (urls?.identity) { - return urls.identity; - } - if (urls?.base) { - return urls.base + "/identity"; - } - return "https://identity.bitwarden.com"; - } - - // =================================================================== - // Additional State Methods - // =================================================================== - - async getLocale(options?: StorageOptions): Promise { - return await this.storageService.get("locale"); - } - - async setLocale(value: string, options?: StorageOptions): Promise { - await this.storageService.save("locale", value); - } - - async getInstalledVersion(options?: StorageOptions): Promise { - return await this.storageService.get("installedVersion"); - } - - async setInstalledVersion(value: string, options?: StorageOptions): Promise { - await this.storageService.save("installedVersion", value); - } - - // =================================================================== - // Window Settings (for WindowMain) - // =================================================================== - - async getWindow(options?: StorageOptions): Promise { - return await this.storageService.get(StorageKeys.window); - } - - async setWindow(value: any, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.window, value); - } - - async getEnableAlwaysOnTop(options?: StorageOptions): Promise { - return (await this.storageService.get(StorageKeys.enableAlwaysOnTop)) ?? false; - } - - async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.enableAlwaysOnTop, value); - } - - // =================================================================== - // Tray Settings (for TrayMain) - // =================================================================== - - async getEnableTray(options?: StorageOptions): Promise { - return (await this.storageService.get(StorageKeys.enableTray)) ?? false; - } - - async setEnableTray(value: boolean, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.enableTray, value); - } - - async getEnableMinimizeToTray(options?: StorageOptions): Promise { - return (await this.storageService.get(StorageKeys.enableMinimizeToTray)) ?? false; - } - - async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.enableMinimizeToTray, value); - } - - async getEnableCloseToTray(options?: StorageOptions): Promise { - return (await this.storageService.get(StorageKeys.enableCloseToTray)) ?? false; - } - - async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.enableCloseToTray, value); - } - - async getAlwaysShowDock(options?: StorageOptions): Promise { - return (await this.storageService.get(StorageKeys.alwaysShowDock)) ?? false; - } - - async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise { - await this.storageService.save(StorageKeys.alwaysShowDock, value); - } - - // =================================================================== - // Token Management (replaces TokenService.clearToken()) - // =================================================================== - - async clearAuthTokens(): Promise { - await this.secureStorageService.remove("accessToken"); - await this.secureStorageService.remove("refreshToken"); - await this.secureStorageService.remove("apiKeyClientId"); - await this.secureStorageService.remove("apiKeyClientSecret"); - await this.secureStorageService.remove("twoFactorToken"); - } -} diff --git a/src/services/state-service/state-vNext.service.spec.ts b/src/services/state-service/state.service.spec.ts similarity index 83% rename from src/services/state-service/state-vNext.service.spec.ts rename to src/services/state-service/state.service.spec.ts index 4229b1f6..2339b3a9 100644 --- a/src/services/state-service/state-vNext.service.spec.ts +++ b/src/services/state-service/state.service.spec.ts @@ -1,7 +1,6 @@ import { mock, MockProxy } from "jest-mock-extended"; import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateMigrationService } from "@/jslib/common/src/abstractions/stateMigration.service"; import { StorageService } from "@/jslib/common/src/abstractions/storage.service"; import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; @@ -14,14 +13,15 @@ import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration"; import { StorageKeysVNext as StorageKeys, StoredSecurely } from "@/src/models/state.model"; import { SyncConfiguration } from "@/src/models/syncConfiguration"; -import { StateServiceVNextImplementation } from "./state-vNext.service"; +import { StateServiceImplementation } from "./state.service"; +import { StateMigrationService } from "./stateMigration.service"; -describe("StateServiceVNextImplementation", () => { +describe("StateServiceImplementation", () => { let storageService: MockProxy; let secureStorageService: MockProxy; let logService: MockProxy; let stateMigrationService: MockProxy; - let stateService: StateServiceVNextImplementation; + let stateService: StateServiceImplementation; beforeEach(() => { storageService = mock(); @@ -29,7 +29,7 @@ describe("StateServiceVNextImplementation", () => { logService = mock(); stateMigrationService = mock(); - stateService = new StateServiceVNextImplementation( + stateService = new StateServiceImplementation( storageService, secureStorageService, logService, @@ -446,7 +446,7 @@ describe("StateServiceVNextImplementation", () => { describe("Secure Storage Flag", () => { it("should not separate secrets when useSecureStorageForSecrets is false", async () => { - const insecureStateService = new StateServiceVNextImplementation( + const insecureStateService = new StateServiceImplementation( storageService, secureStorageService, logService, @@ -613,11 +613,7 @@ describe("StateServiceVNextImplementation", () => { base: "https://vault.example.com", api: "https://api.example.com", identity: "https://identity.example.com", - icons: "https://icons.example.com", - notifications: "https://notifications.example.com", - events: "https://events.example.com", webVault: "https://vault.example.com", - keyConnector: null, }; storageService.get.mockResolvedValue(urls); @@ -642,11 +638,7 @@ describe("StateServiceVNextImplementation", () => { base: null, api: "https://api.example.com", identity: null, - icons: null, - notifications: null, - events: null, webVault: null, - keyConnector: null, }; storageService.get.mockResolvedValue(urls); @@ -661,11 +653,7 @@ describe("StateServiceVNextImplementation", () => { base: "https://vault.example.com", api: null, identity: null, - icons: null, - notifications: null, - events: null, webVault: null, - keyConnector: null, }; storageService.get.mockResolvedValue(urls); @@ -688,11 +676,7 @@ describe("StateServiceVNextImplementation", () => { base: null, api: null, identity: "https://identity.example.com", - icons: null, - notifications: null, - events: null, webVault: null, - keyConnector: null, }; storageService.get.mockResolvedValue(urls); @@ -707,11 +691,7 @@ describe("StateServiceVNextImplementation", () => { base: "https://vault.example.com", api: null, identity: null, - icons: null, - notifications: null, - events: null, webVault: null, - keyConnector: null, }; storageService.get.mockResolvedValue(urls); @@ -747,5 +727,135 @@ describe("StateServiceVNextImplementation", () => { // Verify that all 5 token types are removed expect(secureStorageService.remove).toHaveBeenCalledTimes(5); }); + + describe("Access Token", () => { + it("should get access token from secure storage", async () => { + const token = "test-access-token"; + secureStorageService.get.mockResolvedValue(token); + + const result = await stateService.getAccessToken(); + + expect(result).toBe(token); + expect(secureStorageService.get).toHaveBeenCalledWith("accessToken"); + }); + + it("should set access token in secure storage", async () => { + const token = "test-access-token"; + + await stateService.setAccessToken(token); + + expect(secureStorageService.save).toHaveBeenCalledWith("accessToken", token); + }); + + it("should remove access token when set to null", async () => { + await stateService.setAccessToken(null); + + expect(secureStorageService.remove).toHaveBeenCalledWith("accessToken"); + }); + }); + + describe("Refresh Token", () => { + it("should get refresh token from secure storage", async () => { + const token = "test-refresh-token"; + secureStorageService.get.mockResolvedValue(token); + + const result = await stateService.getRefreshToken(); + + expect(result).toBe(token); + expect(secureStorageService.get).toHaveBeenCalledWith("refreshToken"); + }); + + it("should set refresh token in secure storage", async () => { + const token = "test-refresh-token"; + + await stateService.setRefreshToken(token); + + expect(secureStorageService.save).toHaveBeenCalledWith("refreshToken", token); + }); + + it("should remove refresh token when set to null", async () => { + await stateService.setRefreshToken(null); + + expect(secureStorageService.remove).toHaveBeenCalledWith("refreshToken"); + }); + }); + + describe("API Key Client ID", () => { + it("should get API key client ID from secure storage", async () => { + const clientId = "organization.test-id"; + secureStorageService.get.mockResolvedValue(clientId); + + const result = await stateService.getApiKeyClientId(); + + expect(result).toBe(clientId); + expect(secureStorageService.get).toHaveBeenCalledWith("apiKeyClientId"); + }); + + it("should set API key client ID in secure storage", async () => { + const clientId = "organization.test-id"; + + await stateService.setApiKeyClientId(clientId); + + expect(secureStorageService.save).toHaveBeenCalledWith("apiKeyClientId", clientId); + }); + + it("should remove API key client ID when set to null", async () => { + await stateService.setApiKeyClientId(null); + + expect(secureStorageService.remove).toHaveBeenCalledWith("apiKeyClientId"); + }); + }); + + describe("API Key Client Secret", () => { + it("should get API key client secret from secure storage", async () => { + const clientSecret = "test-secret"; + secureStorageService.get.mockResolvedValue(clientSecret); + + const result = await stateService.getApiKeyClientSecret(); + + expect(result).toBe(clientSecret); + expect(secureStorageService.get).toHaveBeenCalledWith("apiKeyClientSecret"); + }); + + it("should set API key client secret in secure storage", async () => { + const clientSecret = "test-secret"; + + await stateService.setApiKeyClientSecret(clientSecret); + + expect(secureStorageService.save).toHaveBeenCalledWith("apiKeyClientSecret", clientSecret); + }); + + it("should remove API key client secret when set to null", async () => { + await stateService.setApiKeyClientSecret(null); + + expect(secureStorageService.remove).toHaveBeenCalledWith("apiKeyClientSecret"); + }); + }); + + describe("Entity ID", () => { + it("should get entity ID from storage", async () => { + const entityId = "test-entity-id"; + storageService.get.mockResolvedValue(entityId); + + const result = await stateService.getEntityId(); + + expect(result).toBe(entityId); + expect(storageService.get).toHaveBeenCalledWith("entityId"); + }); + + it("should set entity ID in storage", async () => { + const entityId = "test-entity-id"; + + await stateService.setEntityId(entityId); + + expect(storageService.save).toHaveBeenCalledWith("entityId", entityId); + }); + + it("should remove entity ID when set to null", async () => { + await stateService.setEntityId(null); + + expect(storageService.remove).toHaveBeenCalledWith("entityId"); + }); + }); }); }); diff --git a/src/services/state-service/state.service.ts b/src/services/state-service/state.service.ts index 02d04306..3179a2ef 100644 --- a/src/services/state-service/state.service.ts +++ b/src/services/state-service/state.service.ts @@ -1,43 +1,58 @@ import { LogService } from "@/jslib/common/src/abstractions/log.service"; -import { StateMigrationService } from "@/jslib/common/src/abstractions/stateMigration.service"; import { StorageService } from "@/jslib/common/src/abstractions/storage.service"; -import { StateFactory } from "@/jslib/common/src/factories/stateFactory"; import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls"; -import { GlobalState } from "@/jslib/common/src/models/domain/globalState"; import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"; -import { StateService as BaseStateService } from "@/jslib/common/src/services/state.service"; import { StateService as StateServiceAbstraction } from "@/src/abstractions/state.service"; import { DirectoryType } from "@/src/enums/directoryType"; import { IConfiguration } from "@/src/models/IConfiguration"; -import { Account } from "@/src/models/account"; import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration"; import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; import { LdapConfiguration } from "@/src/models/ldapConfiguration"; import { OktaConfiguration } from "@/src/models/oktaConfiguration"; import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration"; import { - SecureStorageKeysLegacy as SecureStorageKeys, + SecureStorageKeysVNext as SecureStorageKeys, + StorageKeysVNext as StorageKeys, StoredSecurely, - TempKeys as keys, } from "@/src/models/state.model"; import { SyncConfiguration } from "@/src/models/syncConfiguration"; -export class StateService - extends BaseStateService - implements StateServiceAbstraction -{ +import { StateMigrationService } from "./stateMigration.service"; + +export class StateServiceImplementation implements StateServiceAbstraction { constructor( protected storageService: StorageService, protected secureStorageService: StorageService, protected logService: LogService, protected stateMigrationService: StateMigrationService, private useSecureStorageForSecrets = true, - protected stateFactory: StateFactory, - ) { - super(storageService, secureStorageService, logService, stateMigrationService, stateFactory); + ) {} + + async init(): Promise { + if (await this.stateMigrationService.needsMigration()) { + await this.stateMigrationService.migrate(); + } } + async clean(options?: StorageOptions): Promise { + // Clear all directory settings and configurations + // but preserve version and environment settings + await this.setDirectoryType(null); + await this.setOrganizationId(null); + await this.setSync(null); + await this.setLdapConfiguration(null); + await this.setGsuiteConfiguration(null); + await this.setEntraConfiguration(null); + await this.setOktaConfiguration(null); + await this.setOneLoginConfiguration(null); + await this.clearSyncSettings(true); + } + + // =================================================================== + // Directory Configuration Methods + // =================================================================== + async getDirectory(type: DirectoryType): Promise { const config = await this.getConfiguration(type); if (config == null) { @@ -45,24 +60,24 @@ export class StateService } if (this.useSecureStorageForSecrets) { - // Do not introduce secrets into the in-memory account object + // Create a copy to avoid modifying the cached config const configWithSecrets = Object.assign({}, config); switch (type) { case DirectoryType.Ldap: - (configWithSecrets as any).password = await this.getLdapKey(); + (configWithSecrets as any).password = await this.getLdapSecret(); break; case DirectoryType.EntraID: - (configWithSecrets as any).key = await this.getEntraKey(); + (configWithSecrets as any).key = await this.getEntraSecret(); break; case DirectoryType.Okta: - (configWithSecrets as any).token = await this.getOktaKey(); + (configWithSecrets as any).token = await this.getOktaSecret(); break; case DirectoryType.GSuite: - (configWithSecrets as any).privateKey = await this.getGsuiteKey(); + (configWithSecrets as any).privateKey = await this.getGsuiteSecret(); break; case DirectoryType.OneLogin: - (configWithSecrets as any).clientSecret = await this.getOneLoginKey(); + (configWithSecrets as any).clientSecret = await this.getOneLoginSecret(); break; } @@ -85,21 +100,21 @@ export class StateService switch (type) { case DirectoryType.Ldap: { const ldapConfig = config as LdapConfiguration; - await this.setLdapKey(ldapConfig.password); + await this.setLdapSecret(ldapConfig.password); ldapConfig.password = StoredSecurely; await this.setLdapConfiguration(ldapConfig); break; } case DirectoryType.EntraID: { const entraConfig = config as EntraIdConfiguration; - await this.setEntraKey(entraConfig.key); + await this.setEntraSecret(entraConfig.key); entraConfig.key = StoredSecurely; await this.setEntraConfiguration(entraConfig); break; } case DirectoryType.Okta: { const oktaConfig = config as OktaConfiguration; - await this.setOktaKey(oktaConfig.token); + await this.setOktaSecret(oktaConfig.token); oktaConfig.token = StoredSecurely; await this.setOktaConfiguration(oktaConfig); break; @@ -107,10 +122,10 @@ export class StateService case DirectoryType.GSuite: { const gsuiteConfig = config as GSuiteConfiguration; if (gsuiteConfig.privateKey == null) { - await this.setGsuiteKey(null); + await this.setGsuiteSecret(null); } else { const normalizedPrivateKey = gsuiteConfig.privateKey.replace(/\\n/g, "\n"); - await this.setGsuiteKey(normalizedPrivateKey); + await this.setGsuiteSecret(normalizedPrivateKey); gsuiteConfig.privateKey = StoredSecurely; } await this.setGsuiteConfiguration(gsuiteConfig); @@ -118,7 +133,7 @@ export class StateService } case DirectoryType.OneLogin: { const oneLoginConfig = config as OneLoginConfiguration; - await this.setOneLoginKey(oneLoginConfig.clientSecret); + await this.setOneLoginSecret(oneLoginConfig.clientSecret); oneLoginConfig.clientSecret = StoredSecurely; await this.setOneLoginConfiguration(oneLoginConfig); break; @@ -127,125 +142,6 @@ export class StateService } } - private async getLdapKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options.userId}_${SecureStorageKeys.ldap}`, - ); - } - - private async setLdapKey(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - await this.secureStorageService.save( - `${options.userId}_${SecureStorageKeys.ldap}`, - value, - options, - ); - } - - private async getGsuiteKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options.userId}_${SecureStorageKeys.gsuite}`, - ); - } - - private async setGsuiteKey(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - await this.secureStorageService.save( - `${options.userId}_${SecureStorageKeys.gsuite}`, - value, - options, - ); - } - - private async getEntraKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - - const entraKey = await this.secureStorageService.get( - `${options.userId}_${SecureStorageKeys.entra}`, - ); - - if (entraKey != null) { - return entraKey; - } - - return await this.secureStorageService.get( - `${options.userId}_${SecureStorageKeys.azure}`, - ); - } - - private async setEntraKey(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - await this.secureStorageService.save( - `${options.userId}_${SecureStorageKeys.entra}`, - value, - options, - ); - } - - private async getOktaKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options.userId}_${SecureStorageKeys.okta}`, - ); - } - - private async setOktaKey(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - await this.secureStorageService.save( - `${options.userId}_${SecureStorageKeys.okta}`, - value, - options, - ); - } - - private async getOneLoginKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options.userId}_${SecureStorageKeys.oneLogin}`, - ); - } - - private async setOneLoginKey(value: string, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - await this.secureStorageService.save( - `${options.userId}_${SecureStorageKeys.oneLogin}`, - value, - options, - ); - } - async getConfiguration(type: DirectoryType): Promise { switch (type) { case DirectoryType.Ldap: @@ -261,112 +157,135 @@ export class StateService } } + // =================================================================== + // Secret Storage Methods (Secure Storage) + // =================================================================== + + private async getLdapSecret(): Promise { + return await this.secureStorageService.get(SecureStorageKeys.ldap); + } + + private async setLdapSecret(value: string): Promise { + if (value == null) { + await this.secureStorageService.remove(SecureStorageKeys.ldap); + } else { + await this.secureStorageService.save(SecureStorageKeys.ldap, value); + } + } + + private async getGsuiteSecret(): Promise { + return await this.secureStorageService.get(SecureStorageKeys.gsuite); + } + + private async setGsuiteSecret(value: string): Promise { + if (value == null) { + await this.secureStorageService.remove(SecureStorageKeys.gsuite); + } else { + await this.secureStorageService.save(SecureStorageKeys.gsuite, value); + } + } + + private async getEntraSecret(): Promise { + // Try new key first, fall back to old azure key for backwards compatibility + const entraKey = await this.secureStorageService.get(SecureStorageKeys.entra); + if (entraKey != null) { + return entraKey; + } + return await this.secureStorageService.get(SecureStorageKeys.azure); + } + + private async setEntraSecret(value: string): Promise { + if (value == null) { + await this.secureStorageService.remove(SecureStorageKeys.entra); + await this.secureStorageService.remove(SecureStorageKeys.azure); + } else { + await this.secureStorageService.save(SecureStorageKeys.entra, value); + } + } + + private async getOktaSecret(): Promise { + return await this.secureStorageService.get(SecureStorageKeys.okta); + } + + private async setOktaSecret(value: string): Promise { + if (value == null) { + await this.secureStorageService.remove(SecureStorageKeys.okta); + } else { + await this.secureStorageService.save(SecureStorageKeys.okta, value); + } + } + + private async getOneLoginSecret(): Promise { + return await this.secureStorageService.get(SecureStorageKeys.oneLogin); + } + + private async setOneLoginSecret(value: string): Promise { + if (value == null) { + await this.secureStorageService.remove(SecureStorageKeys.oneLogin); + } else { + await this.secureStorageService.save(SecureStorageKeys.oneLogin, value); + } + } + + // =================================================================== + // Directory-Specific Configuration Methods + // =================================================================== + async getLdapConfiguration(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directoryConfigurations?.ldap; + return await this.storageService.get(StorageKeys.directory_ldap); } async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directoryConfigurations.ldap = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.directory_ldap, value); } async getGsuiteConfiguration(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directoryConfigurations?.gsuite; + return await this.storageService.get(StorageKeys.directory_gsuite); } async setGsuiteConfiguration( value: GSuiteConfiguration, options?: StorageOptions, ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directoryConfigurations.gsuite = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.directory_gsuite, value); } async getEntraConfiguration(options?: StorageOptions): Promise { - const entraConfig = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directoryConfigurations?.entra; - - if (entraConfig != null) { - return entraConfig; - } - - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directoryConfigurations?.azure; + return await this.storageService.get(StorageKeys.directory_entra); } async setEntraConfiguration( value: EntraIdConfiguration, options?: StorageOptions, ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directoryConfigurations.entra = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.directory_entra, value); } async getOktaConfiguration(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directoryConfigurations?.okta; + return await this.storageService.get(StorageKeys.directory_okta); } async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directoryConfigurations.okta = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.directory_okta, value); } async getOneLoginConfiguration(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directoryConfigurations?.oneLogin; + return await this.storageService.get(StorageKeys.directory_onelogin); } async setOneLoginConfiguration( value: OneLoginConfiguration, options?: StorageOptions, ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directoryConfigurations.oneLogin = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.directory_onelogin, value); } + // =================================================================== + // Directory Settings Methods + // =================================================================== + async getOrganizationId(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.organizationId; + return await this.storageService.get(StorageKeys.organizationId); } async setOrganizationId(value: string, options?: StorageOptions): Promise { @@ -374,38 +293,19 @@ export class StateService if (currentId !== value) { await this.clearSyncSettings(); } - - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.organizationId = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.organizationId, value); } async getSync(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.sync; + return await this.storageService.get(StorageKeys.sync); } async setSync(value: SyncConfiguration, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.sync = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.sync, value); } async getDirectoryType(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.directoryType; + return await this.storageService.get(StorageKeys.directoryType); } async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise { @@ -413,117 +313,60 @@ export class StateService if (value !== currentType) { await this.clearSyncSettings(); } - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.directoryType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(StorageKeys.directoryType, value); } async getLastUserSync(options?: StorageOptions): Promise { - const userSyncDate = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.lastUserSync; - return userSyncDate ? new Date(userSyncDate) : null; + const dateString = await this.storageService.get(SecureStorageKeys.lastUserSync); + return dateString ? new Date(dateString) : null; } async setLastUserSync(value: Date, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.lastUserSync = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(SecureStorageKeys.lastUserSync, value); } async getLastGroupSync(options?: StorageOptions): Promise { - const groupSyncDate = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.lastGroupSync; - return groupSyncDate ? new Date(groupSyncDate) : null; + const dateString = await this.storageService.get(SecureStorageKeys.lastGroupSync); + return dateString ? new Date(dateString) : null; } async setLastGroupSync(value: Date, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.lastGroupSync = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(SecureStorageKeys.lastGroupSync, value); } async getLastSyncHash(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.lastSyncHash; + return await this.storageService.get(SecureStorageKeys.lastSyncHash); } async setLastSyncHash(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.lastSyncHash = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(SecureStorageKeys.lastSyncHash, value); } async getSyncingDir(options?: StorageOptions): Promise { - return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) - ?.directorySettings?.syncingDir; + return await this.storageService.get(StorageKeys.syncingDir); } async setSyncingDir(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, this.defaultInMemoryOptions), - ); - account.directorySettings.syncingDir = value; - await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + await this.storageService.save(StorageKeys.syncingDir, value); } async getUserDelta(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.userDelta; + return await this.storageService.get(SecureStorageKeys.userDelta); } async setUserDelta(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.userDelta = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(SecureStorageKeys.userDelta, value); } async getGroupDelta(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.directorySettings?.groupDelta; + return await this.storageService.get(SecureStorageKeys.groupDelta); } async setGroupDelta(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.directorySettings.groupDelta = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); + await this.storageService.save(SecureStorageKeys.groupDelta, value); } - async clearSyncSettings(hashToo = false) { + async clearSyncSettings(hashToo = false): Promise { await this.setUserDelta(null); await this.setGroupDelta(null); await this.setLastGroupSync(null); @@ -533,62 +376,194 @@ export class StateService } } - protected async scaffoldNewAccountStorage(account: Account): Promise { - await this.scaffoldNewAccountDiskStorage(account); - } - - protected async scaffoldNewAccountDiskStorage(account: Account): Promise { - const storageOptions = this.reconcileOptions( - { userId: account.profile.userId }, - await this.defaultOnDiskLocalOptions(), - ); - - const storedAccount = await this.getAccount(storageOptions); - if (storedAccount != null) { - account.settings = storedAccount.settings; - account.directorySettings = storedAccount.directorySettings; - account.directoryConfigurations = storedAccount.directoryConfigurations; - } else if (await this.hasTemporaryStorage()) { - // If migrating to state V2 with an no actively authed account we store temporary data to be copied on auth - this will only be run once. - account.settings = await this.storageService.get(keys.tempAccountSettings); - account.directorySettings = await this.storageService.get(keys.tempDirectorySettings); - account.directoryConfigurations = await this.storageService.get( - keys.tempDirectoryConfigs, - ); - await this.storageService.remove(keys.tempAccountSettings); - await this.storageService.remove(keys.tempDirectorySettings); - await this.storageService.remove(keys.tempDirectoryConfigs); - } - - await this.saveAccount(account, storageOptions); - } - - protected async pushAccounts(): Promise { - if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) { - this.accountsSubject.next(null); - return; - } - this.accountsSubject.next(this.state.accounts); - } - - protected async hasTemporaryStorage(): Promise { - return ( - (await this.storageService.has(keys.tempAccountSettings)) || - (await this.storageService.has(keys.tempDirectorySettings)) || - (await this.storageService.has(keys.tempDirectoryConfigs)) - ); - } - - protected resetAccount(account: Account) { - const persistentAccountInformation = { - settings: account.settings, - directorySettings: account.directorySettings, - directoryConfigurations: account.directoryConfigurations, - }; - return Object.assign(this.createAccount(), persistentAccountInformation); - } + // =================================================================== + // Environment URLs + // =================================================================== async getEnvironmentUrls(options?: StorageOptions): Promise { - return this.getGlobalEnvironmentUrls(options); + return await this.storageService.get(StorageKeys.environmentUrls); + } + + async setEnvironmentUrls(value: EnvironmentUrls): Promise { + await this.storageService.save(StorageKeys.environmentUrls, value); + } + + async getApiUrl(options?: StorageOptions): Promise { + const urls = await this.getEnvironmentUrls(options); + if (urls?.api) { + return urls.api; + } + if (urls?.base) { + return urls.base + "/api"; + } + return "https://api.bitwarden.com"; + } + + async getIdentityUrl(options?: StorageOptions): Promise { + const urls = await this.getEnvironmentUrls(options); + if (urls?.identity) { + return urls.identity; + } + if (urls?.base) { + return urls.base + "/identity"; + } + return "https://identity.bitwarden.com"; + } + + // =================================================================== + // Additional State Methods + // =================================================================== + + async getLocale(options?: StorageOptions): Promise { + return await this.storageService.get("locale"); + } + + async setLocale(value: string, options?: StorageOptions): Promise { + await this.storageService.save("locale", value); + } + + async getInstalledVersion(options?: StorageOptions): Promise { + return await this.storageService.get("installedVersion"); + } + + async setInstalledVersion(value: string, options?: StorageOptions): Promise { + await this.storageService.save("installedVersion", value); + } + + // =================================================================== + // Window Settings (for WindowMain) + // =================================================================== + + async getWindow(options?: StorageOptions): Promise { + return await this.storageService.get(StorageKeys.window); + } + + async setWindow(value: any, options?: StorageOptions): Promise { + await this.storageService.save(StorageKeys.window, value); + } + + async getEnableAlwaysOnTop(options?: StorageOptions): Promise { + return (await this.storageService.get(StorageKeys.enableAlwaysOnTop)) ?? false; + } + + async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise { + await this.storageService.save(StorageKeys.enableAlwaysOnTop, value); + } + + // =================================================================== + // Tray Settings (for TrayMain) + // =================================================================== + + async getEnableTray(options?: StorageOptions): Promise { + return (await this.storageService.get(StorageKeys.enableTray)) ?? false; + } + + async setEnableTray(value: boolean, options?: StorageOptions): Promise { + await this.storageService.save(StorageKeys.enableTray, value); + } + + async getEnableMinimizeToTray(options?: StorageOptions): Promise { + return (await this.storageService.get(StorageKeys.enableMinimizeToTray)) ?? false; + } + + async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise { + await this.storageService.save(StorageKeys.enableMinimizeToTray, value); + } + + async getEnableCloseToTray(options?: StorageOptions): Promise { + return (await this.storageService.get(StorageKeys.enableCloseToTray)) ?? false; + } + + async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise { + await this.storageService.save(StorageKeys.enableCloseToTray, value); + } + + async getAlwaysShowDock(options?: StorageOptions): Promise { + return (await this.storageService.get(StorageKeys.alwaysShowDock)) ?? false; + } + + async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise { + await this.storageService.save(StorageKeys.alwaysShowDock, value); + } + + // =================================================================== + // Token Management (replaces TokenService.clearToken()) + // =================================================================== + + async clearAuthTokens(): Promise { + await this.secureStorageService.remove("accessToken"); + await this.secureStorageService.remove("refreshToken"); + await this.secureStorageService.remove("apiKeyClientId"); + await this.secureStorageService.remove("apiKeyClientSecret"); + await this.secureStorageService.remove("twoFactorToken"); + } + + async getAccessToken(options?: StorageOptions): Promise { + return await this.secureStorageService.get("accessToken"); + } + + async setAccessToken(value: string, options?: StorageOptions): Promise { + if (value == null) { + await this.secureStorageService.remove("accessToken"); + } else { + await this.secureStorageService.save("accessToken", value); + } + } + + async getRefreshToken(options?: StorageOptions): Promise { + return await this.secureStorageService.get("refreshToken"); + } + + async setRefreshToken(value: string, options?: StorageOptions): Promise { + if (value == null) { + await this.secureStorageService.remove("refreshToken"); + } else { + await this.secureStorageService.save("refreshToken", value); + } + } + + async getApiKeyClientId(options?: StorageOptions): Promise { + return await this.secureStorageService.get("apiKeyClientId"); + } + + async setApiKeyClientId(value: string, options?: StorageOptions): Promise { + if (value == null) { + await this.secureStorageService.remove("apiKeyClientId"); + } else { + await this.secureStorageService.save("apiKeyClientId", value); + } + } + + async getApiKeyClientSecret(options?: StorageOptions): Promise { + return await this.secureStorageService.get("apiKeyClientSecret"); + } + + async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise { + if (value == null) { + await this.secureStorageService.remove("apiKeyClientSecret"); + } else { + await this.secureStorageService.save("apiKeyClientSecret", value); + } + } + + async getIsAuthenticated(options?: StorageOptions): Promise { + // Check if access token exists + const token = await this.getAccessToken(options); + return token != null; + } + + async getEntityId(options?: StorageOptions): Promise { + return await this.storageService.get("entityId"); + } + + async setEntityId(value: string, options?: StorageOptions): Promise { + if (value == null) { + await this.storageService.remove("entityId"); + } else { + await this.storageService.save("entityId", value); + } } } + +// Re-export the abstraction for convenience +export { StateService } from "@/src/abstractions/state.service"; diff --git a/src/services/state-service/stateMigration.service.ts b/src/services/state-service/stateMigration.service.ts index 7194c42b..8b1794d8 100644 --- a/src/services/state-service/stateMigration.service.ts +++ b/src/services/state-service/stateMigration.service.ts @@ -1,8 +1,10 @@ +import { StorageService } from "@/jslib/common/src/abstractions/storage.service"; +import { HtmlStorageLocation } from "@/jslib/common/src/enums/htmlStorageLocation"; import { StateVersion } from "@/jslib/common/src/enums/stateVersion"; -import { StateMigrationService as BaseStateMigrationService } from "@/jslib/common/src/services/stateMigration.service"; +import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"; import { DirectoryType } from "@/src/enums/directoryType"; -import { Account, DirectoryConfigurations, DirectorySettings } from "@/src/models/account"; +import { DirectoryConfigurations, DirectorySettings } from "@/src/models/account"; import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration"; import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; import { LdapConfiguration } from "@/src/models/ldapConfiguration"; @@ -13,10 +15,22 @@ import { MigrationKeys as Keys, MigrationStateKeys as StateKeys, SecureStorageKeysMigration as SecureStorageKeys, + SecureStorageKeysVNext, + StorageKeysVNext, } from "@/src/models/state.model"; import { SyncConfiguration } from "@/src/models/syncConfiguration"; -export class StateMigrationService extends BaseStateMigrationService { +export class StateMigrationService { + constructor( + protected storageService: StorageService, + protected secureStorageService: StorageService, + ) {} + + async needsMigration(): Promise { + const currentStateVersion = await this.getCurrentStateVersion(); + return currentStateVersion == null || currentStateVersion < StateVersion.Latest; + } + async migrate(): Promise { let currentStateVersion = await this.getCurrentStateVersion(); while (currentStateVersion < StateVersion.Latest) { @@ -56,14 +70,12 @@ export class StateMigrationService extends BaseStateMigrationService { } protected async migrateStateFrom1To2(useSecureStorageForSecrets = true): Promise { - // Grabbing a couple of key settings before they get cleared by the base migration + // Grabbing a couple of key settings before they get cleared by the migration const userId = await this.get(Keys.entityId); const clientId = await this.get(ClientKeys.clientId); const clientSecret = await this.get(ClientKeys.clientSecret); - await super.migrateStateFrom1To2(); - - // Setup reusable method for clearing keys since we will want to do that regardless of if there is an active authenticated session + // Setup reusable method for clearing keys const clearDirectoryConnectorV1Keys = async () => { for (const key in Keys) { if (key == null) { @@ -78,7 +90,7 @@ export class StateMigrationService extends BaseStateMigrationService { } }; - // Initialize typed objects from key/value pairs in storage to either be saved temporarily until an account is authed or applied to the active account + // Initialize typed objects from key/value pairs in storage const getDirectoryConfig = async (type: DirectoryType) => await this.get(SecureStorageKeys.directoryConfigPrefix + type); const directoryConfigs: DirectoryConfigurations = { @@ -104,16 +116,19 @@ export class StateMigrationService extends BaseStateMigrationService { groupDelta: await this.get(Keys.groupDelta), }; - // (userId == null) = no authed account, stored data temporarily to be applied and cleared on next auth - // (userId != null) = authed account known, applied stored data to it and do not save temp data + // (userId == null) = no authed account, store data temporarily to be applied on next auth + // (userId != null) = authed account known, apply stored data to it if (userId == null) { await this.set(Keys.tempDirectoryConfigs, directoryConfigs); await this.set(Keys.tempDirectorySettings, directorySettings); await clearDirectoryConnectorV1Keys(); + + // Set initial state version + await this.set(StorageKeysVNext.stateVersion, StateVersion.Two); return; } - const account = await this.get(userId); + const account = await this.get(userId); account.directoryConfigurations = directoryConfigs; account.directorySettings = directorySettings; account.profile = { @@ -140,36 +155,76 @@ export class StateMigrationService extends BaseStateMigrationService { } } } + + // Update state version + const globals = await this.getGlobals(); + if (globals) { + globals.stateVersion = StateVersion.Two; + await this.set(StateKeys.global, globals); + } else { + await this.set(StorageKeysVNext.stateVersion, StateVersion.Two); + } } + protected async migrateStateFrom2To3(useSecureStorageForSecrets = true): Promise { if (useSecureStorageForSecrets) { const authenticatedUserIds = await this.get(StateKeys.authenticatedAccounts); + if (authenticatedUserIds && authenticatedUserIds.length > 0) { + await Promise.all( + authenticatedUserIds.map(async (userId) => { + const account = await this.get(userId); + + // Fix for userDelta and groupDelta being put into secure storage when they should not have + if (await this.secureStorageService.has(`${userId}_${Keys.userDelta}`)) { + account.directorySettings.userDelta = await this.secureStorageService.get( + `${userId}_${Keys.userDelta}`, + ); + await this.secureStorageService.remove(`${userId}_${Keys.userDelta}`); + } + if (await this.secureStorageService.has(`${userId}_${Keys.groupDelta}`)) { + account.directorySettings.groupDelta = await this.secureStorageService.get( + `${userId}_${Keys.groupDelta}`, + ); + await this.secureStorageService.remove(`${userId}_${Keys.groupDelta}`); + } + await this.set(userId, account); + }), + ); + } + } + + const globals = await this.getGlobals(); + if (globals) { + globals.stateVersion = StateVersion.Three; + await this.set(StateKeys.global, globals); + } else { + await this.set(StorageKeysVNext.stateVersion, StateVersion.Three); + } + } + + protected async migrateStateFrom3To4(): Promise { + const authenticatedUserIds = await this.get(StateKeys.authenticatedAccounts); + + if (authenticatedUserIds && authenticatedUserIds.length > 0) { await Promise.all( authenticatedUserIds.map(async (userId) => { - const account = await this.get(userId); - - // Fix for userDelta and groupDelta being put into secure storage when they should not have - if (await this.secureStorageService.has(`${userId}_${Keys.userDelta}`)) { - account.directorySettings.userDelta = await this.secureStorageService.get( - `${userId}_${Keys.userDelta}`, - ); - await this.secureStorageService.remove(`${userId}_${Keys.userDelta}`); + const account = await this.get(userId); + if (account?.profile?.everBeenUnlocked != null) { + delete account.profile.everBeenUnlocked; + return this.set(userId, account); } - if (await this.secureStorageService.has(`${userId}_${Keys.groupDelta}`)) { - account.directorySettings.groupDelta = await this.secureStorageService.get( - `${userId}_${Keys.groupDelta}`, - ); - await this.secureStorageService.remove(`${userId}_${Keys.groupDelta}`); - } - await this.set(userId, account); }), ); } const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Three; - await this.set(StateKeys.global, globals); + if (globals) { + globals.stateVersion = StateVersion.Four; + await this.set(StateKeys.global, globals); + } else { + await this.set(StorageKeysVNext.stateVersion, StateVersion.Four); + } } /** @@ -193,86 +248,88 @@ export class StateMigrationService extends BaseStateMigrationService { authenticatedUserIds.length === 0 ) { // No accounts to migrate, just update version - const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Five; - await this.set(StateKeys.global, globals); + await this.set(StorageKeysVNext.stateVersion, StateVersion.Five); return; } // DC is single-user, so we take the first (and likely only) account const userId = authenticatedUserIds[0]; - const account = await this.get(userId); + const account = await this.get(userId); if (!account) { // No account data found, just update version - const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Five; - await this.set(StateKeys.global, globals); + await this.set(StorageKeysVNext.stateVersion, StateVersion.Five); return; } // Migrate directory configurations to flat structure if (account.directoryConfigurations) { if (account.directoryConfigurations.ldap) { - await this.set("directory_ldap", account.directoryConfigurations.ldap); + await this.set(StorageKeysVNext.directory_ldap, account.directoryConfigurations.ldap); } if (account.directoryConfigurations.gsuite) { - await this.set("directory_gsuite", account.directoryConfigurations.gsuite); + await this.set(StorageKeysVNext.directory_gsuite, account.directoryConfigurations.gsuite); } if (account.directoryConfigurations.entra) { - await this.set("directory_entra", account.directoryConfigurations.entra); + await this.set(StorageKeysVNext.directory_entra, account.directoryConfigurations.entra); } else if (account.directoryConfigurations.azure) { // Backwards compatibility: migrate azure to entra - await this.set("directory_entra", account.directoryConfigurations.azure); + await this.set(StorageKeysVNext.directory_entra, account.directoryConfigurations.azure); } if (account.directoryConfigurations.okta) { - await this.set("directory_okta", account.directoryConfigurations.okta); + await this.set(StorageKeysVNext.directory_okta, account.directoryConfigurations.okta); } if (account.directoryConfigurations.oneLogin) { - await this.set("directory_onelogin", account.directoryConfigurations.oneLogin); + await this.set( + StorageKeysVNext.directory_onelogin, + account.directoryConfigurations.oneLogin, + ); } } // Migrate directory settings to flat structure if (account.directorySettings) { if (account.directorySettings.organizationId) { - await this.set("organizationId", account.directorySettings.organizationId); + await this.set(StorageKeysVNext.organizationId, account.directorySettings.organizationId); } if (account.directorySettings.directoryType != null) { - await this.set("directoryType", account.directorySettings.directoryType); + await this.set(StorageKeysVNext.directoryType, account.directorySettings.directoryType); } if (account.directorySettings.sync) { - await this.set("sync", account.directorySettings.sync); + await this.set(StorageKeysVNext.sync, account.directorySettings.sync); } if (account.directorySettings.lastUserSync) { - await this.set("lastUserSync", account.directorySettings.lastUserSync); + await this.set(SecureStorageKeysVNext.lastUserSync, account.directorySettings.lastUserSync); } if (account.directorySettings.lastGroupSync) { - await this.set("lastGroupSync", account.directorySettings.lastGroupSync); + await this.set( + SecureStorageKeysVNext.lastGroupSync, + account.directorySettings.lastGroupSync, + ); } if (account.directorySettings.lastSyncHash) { - await this.set("lastSyncHash", account.directorySettings.lastSyncHash); + await this.set(SecureStorageKeysVNext.lastSyncHash, account.directorySettings.lastSyncHash); } if (account.directorySettings.userDelta) { - await this.set("userDelta", account.directorySettings.userDelta); + await this.set(SecureStorageKeysVNext.userDelta, account.directorySettings.userDelta); } if (account.directorySettings.groupDelta) { - await this.set("groupDelta", account.directorySettings.groupDelta); + await this.set(SecureStorageKeysVNext.groupDelta, account.directorySettings.groupDelta); } if (account.directorySettings.syncingDir != null) { - await this.set("syncingDir", account.directorySettings.syncingDir); + await this.set(StorageKeysVNext.syncingDir, account.directorySettings.syncingDir); } } // Migrate secrets from {userId}_* to secret_* pattern if (useSecureStorageForSecrets) { const oldSecretKeys = [ - { old: `${userId}_${SecureStorageKeys.ldap}`, new: "secret_ldap" }, - { old: `${userId}_${SecureStorageKeys.gsuite}`, new: "secret_gsuite" }, - { old: `${userId}_${SecureStorageKeys.azure}`, new: "secret_azure" }, - { old: `${userId}_${SecureStorageKeys.entra}`, new: "secret_entra" }, - { old: `${userId}_${SecureStorageKeys.okta}`, new: "secret_okta" }, - { old: `${userId}_${SecureStorageKeys.oneLogin}`, new: "secret_onelogin" }, + { old: `${userId}_${SecureStorageKeys.ldap}`, new: SecureStorageKeysVNext.ldap }, + { old: `${userId}_${SecureStorageKeys.gsuite}`, new: SecureStorageKeysVNext.gsuite }, + { old: `${userId}_${SecureStorageKeys.azure}`, new: SecureStorageKeysVNext.azure }, + { old: `${userId}_${SecureStorageKeys.entra}`, new: SecureStorageKeysVNext.entra }, + { old: `${userId}_${SecureStorageKeys.okta}`, new: SecureStorageKeysVNext.okta }, + { old: `${userId}_${SecureStorageKeys.oneLogin}`, new: SecureStorageKeysVNext.oneLogin }, ]; for (const { old: oldKey, new: newKey } of oldSecretKeys) { @@ -291,31 +348,66 @@ export class StateMigrationService extends BaseStateMigrationService { const globals = await this.getGlobals(); if (globals) { if (globals.window) { - await this.set("window", globals.window); + await this.set(StorageKeysVNext.window, globals.window); } if (globals.enableAlwaysOnTop !== undefined) { - await this.set("enableAlwaysOnTop", globals.enableAlwaysOnTop); + await this.set(StorageKeysVNext.enableAlwaysOnTop, globals.enableAlwaysOnTop); } if (globals.enableTray !== undefined) { - await this.set("enableTray", globals.enableTray); + await this.set(StorageKeysVNext.enableTray, globals.enableTray); } if (globals.enableMinimizeToTray !== undefined) { - await this.set("enableMinimizeToTray", globals.enableMinimizeToTray); + await this.set(StorageKeysVNext.enableMinimizeToTray, globals.enableMinimizeToTray); } if (globals.enableCloseToTray !== undefined) { - await this.set("enableCloseToTray", globals.enableCloseToTray); + await this.set(StorageKeysVNext.enableCloseToTray, globals.enableCloseToTray); } if (globals.alwaysShowDock !== undefined) { - await this.set("alwaysShowDock", globals.alwaysShowDock); + await this.set(StorageKeysVNext.alwaysShowDock, globals.alwaysShowDock); } } // Migrate environment URLs from account settings if (account.settings?.environmentUrls) { - await this.set("environmentUrls", account.settings.environmentUrls); + await this.set(StorageKeysVNext.environmentUrls, account.settings.environmentUrls); } - globals.stateVersion = StateVersion.Five; - await this.set(StateKeys.global, globals); + // Set final state version using the new flat key + await this.set(StorageKeysVNext.stateVersion, StateVersion.Five); + } + + // =================================================================== + // Helper Methods + // =================================================================== + + protected get options(): StorageOptions { + return { htmlStorageLocation: HtmlStorageLocation.Local }; + } + + protected get(key: string): Promise { + return this.storageService.get(key, this.options); + } + + protected set(key: string, value: any): Promise { + if (value == null) { + return this.storageService.remove(key, this.options); + } + return this.storageService.save(key, value, this.options); + } + + protected async getGlobals(): Promise { + return await this.get(StateKeys.global); + } + + protected async getCurrentStateVersion(): Promise { + // Try new flat structure first + const flatVersion = await this.get(StorageKeysVNext.stateVersion); + if (flatVersion != null) { + return flatVersion; + } + + // Fall back to old globals structure + const globals = await this.getGlobals(); + return globals?.stateVersion ?? StateVersion.One; } } diff --git a/src/services/sync.service.integration.spec.ts b/src/services/sync.service.integration.spec.ts index 5603ad9a..c2a8b158 100644 --- a/src/services/sync.service.integration.spec.ts +++ b/src/services/sync.service.integration.spec.ts @@ -8,13 +8,12 @@ import { I18nService } from "../../jslib/common/src/abstractions/i18n.service"; import { LogService } from "../../jslib/common/src/abstractions/log.service"; import { getLdapConfiguration, getSyncConfiguration } from "../../utils/openldap/config-fixtures"; import { DirectoryFactoryService } from "../abstractions/directory-factory.service"; -import { StateServiceVNext } from "../abstractions/state-vNext.service"; +import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; import { BatchRequestBuilder } from "./batch-request-builder"; import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; import { SingleRequestBuilder } from "./single-request-builder"; -import { StateService } from "./state-service/state.service"; import { SyncService } from "./sync.service"; import * as constants from "./sync.service"; @@ -25,7 +24,6 @@ describe("SyncService", () => { let logService: MockProxy; let i18nService: MockProxy; let stateService: MockProxy; - let stateServiceVNext: MockProxy; let cryptoFunctionService: MockProxy; let apiService: MockProxy; let messagingService: MockProxy; @@ -42,13 +40,12 @@ describe("SyncService", () => { logService = mock(); i18nService = mock(); stateService = mock(); - stateServiceVNext = mock(); cryptoFunctionService = mock(); apiService = mock(); messagingService = mock(); directoryFactory = mock(); - stateServiceVNext.getApiUrl.mockResolvedValue("https://api.bitwarden.com"); + stateService.getApiUrl.mockResolvedValue("https://api.bitwarden.com"); stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap); stateService.getOrganizationId.mockResolvedValue("fakeId"); @@ -63,7 +60,6 @@ describe("SyncService", () => { apiService, messagingService, i18nService, - stateServiceVNext, stateService, batchRequestBuilder, singleRequestBuilder, diff --git a/src/services/sync.service.spec.ts b/src/services/sync.service.spec.ts index 14b515c1..d5a9f3c6 100644 --- a/src/services/sync.service.spec.ts +++ b/src/services/sync.service.spec.ts @@ -7,14 +7,13 @@ import { ApiService } from "@/jslib/common/src/services/api.service"; import { getSyncConfiguration } from "../../utils/openldap/config-fixtures"; import { DirectoryFactoryService } from "../abstractions/directory-factory.service"; -import { StateServiceVNext } from "../abstractions/state-vNext.service"; +import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; import { BatchRequestBuilder } from "./batch-request-builder"; import { LdapDirectoryService } from "./directory-services/ldap-directory.service"; import { I18nService } from "./i18n.service"; import { SingleRequestBuilder } from "./single-request-builder"; -import { StateService } from "./state-service/state.service"; import { SyncService } from "./sync.service"; import * as constants from "./sync.service"; @@ -26,7 +25,6 @@ describe("SyncService", () => { let apiService: MockProxy; let messagingService: MockProxy; let i18nService: MockProxy; - let stateServiceVNext: MockProxy; let stateService: MockProxy; let directoryFactory: MockProxy; let batchRequestBuilder: MockProxy; @@ -41,13 +39,12 @@ describe("SyncService", () => { apiService = mock(); messagingService = mock(); i18nService = mock(); - stateServiceVNext = mock(); stateService = mock(); directoryFactory = mock(); batchRequestBuilder = mock(); singleRequestBuilder = mock(); - stateServiceVNext.getApiUrl.mockResolvedValue("https://api.bitwarden.com"); + stateService.getApiUrl.mockResolvedValue("https://api.bitwarden.com"); stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap); stateService.getOrganizationId.mockResolvedValue("fakeId"); const mockDirectoryService = mock(); @@ -59,7 +56,6 @@ describe("SyncService", () => { apiService, messagingService, i18nService, - stateServiceVNext, stateService, batchRequestBuilder, singleRequestBuilder, diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 3d13707c..1c108233 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -6,7 +6,6 @@ import { Utils } from "@/jslib/common/src/misc/utils"; import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest"; import { DirectoryFactoryService } from "../abstractions/directory-factory.service"; -import { StateServiceVNext } from "../abstractions/state-vNext.service"; import { StateService } from "../abstractions/state.service"; import { DirectoryType } from "../enums/directoryType"; import { GroupEntry } from "../models/groupEntry"; @@ -31,7 +30,6 @@ export class SyncService { private apiService: ApiService, private messagingService: MessagingService, private i18nService: I18nService, - private stateServiceVNext: StateServiceVNext, private stateService: StateService, private batchRequestBuilder: BatchRequestBuilder, private singleRequestBuilder: SingleRequestBuilder, @@ -119,7 +117,7 @@ export class SyncService { } // TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes - const apiUrl = await this.stateServiceVNext.getApiUrl(); + const apiUrl = await this.stateService.getApiUrl(); let hashLegacy: string = null; const hashBuffLegacy = await this.cryptoFunctionService.hash(apiUrl + reqJson, "sha256"); if (hashBuffLegacy != null) { diff --git a/src/services/token/token.service.ts b/src/services/token/token.service.ts new file mode 100644 index 00000000..bb64ea1c --- /dev/null +++ b/src/services/token/token.service.ts @@ -0,0 +1,90 @@ +import { StorageService } from "@/jslib/common/src/abstractions/storage.service"; + +import { TokenService as ITokenService } from "@/src/abstractions/token.service"; +import { + DecodedToken, + decodeJwt, + tokenNeedsRefresh as checkTokenNeedsRefresh, +} from "@/src/utils/jwt.util"; + +export class TokenService implements ITokenService { + // Storage keys + private TOKEN_KEY = "accessToken"; + private REFRESH_TOKEN_KEY = "refreshToken"; + private CLIENT_ID_KEY = "apiKeyClientId"; + private CLIENT_SECRET_KEY = "apiKeyClientSecret"; + private TWO_FACTOR_TOKEN_KEY = "twoFactorToken"; + + constructor(private secureStorageService: StorageService) {} + + async setTokens( + accessToken: string, + refreshToken: string, + clientIdClientSecret?: [string, string], + ): Promise { + await this.secureStorageService.save(this.TOKEN_KEY, accessToken); + await this.secureStorageService.save(this.REFRESH_TOKEN_KEY, refreshToken); + + if (clientIdClientSecret) { + await this.secureStorageService.save(this.CLIENT_ID_KEY, clientIdClientSecret[0]); + await this.secureStorageService.save(this.CLIENT_SECRET_KEY, clientIdClientSecret[1]); + } + } + + async getToken(): Promise { + return await this.secureStorageService.get(this.TOKEN_KEY); + } + + async getRefreshToken(): Promise { + return await this.secureStorageService.get(this.REFRESH_TOKEN_KEY); + } + + async clearToken(): Promise { + await this.secureStorageService.remove(this.TOKEN_KEY); + await this.secureStorageService.remove(this.REFRESH_TOKEN_KEY); + await this.secureStorageService.remove(this.CLIENT_ID_KEY); + await this.secureStorageService.remove(this.CLIENT_SECRET_KEY); + } + + async getClientId(): Promise { + return await this.secureStorageService.get(this.CLIENT_ID_KEY); + } + + async getClientSecret(): Promise { + return await this.secureStorageService.get(this.CLIENT_SECRET_KEY); + } + + async getTwoFactorToken(): Promise { + return await this.secureStorageService.get(this.TWO_FACTOR_TOKEN_KEY); + } + + async clearTwoFactorToken(): Promise { + await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY); + } + + async decodeToken(token?: string): Promise { + const tokenToUse = token ?? (await this.getToken()); + if (!tokenToUse) { + return null; + } + + try { + return decodeJwt(tokenToUse); + } catch { + return null; + } + } + + async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise { + const token = await this.getToken(); + if (!token) { + return true; + } + + try { + return checkTokenNeedsRefresh(token, minutesBeforeExpiration); + } catch { + return true; + } + } +} diff --git a/src/utils/jwt.util.ts b/src/utils/jwt.util.ts new file mode 100644 index 00000000..f4b6195d --- /dev/null +++ b/src/utils/jwt.util.ts @@ -0,0 +1,44 @@ +export interface DecodedToken { + exp: number; + iat: number; + nbf: number; + sub: string; // user ID + client_id?: string; + [key: string]: any; +} + +export function decodeJwt(token: string): DecodedToken { + // Validate JWT structure (3 parts: header.payload.signature) + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + + // Decode payload (base64url to JSON) + const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + + return JSON.parse(atob(payload)); +} + +export function getTokenExpirationDate(token: string): Date | null { + const decoded = decodeJwt(token); + if (!decoded.exp) { + return null; + } + return new Date(decoded.exp * 1000); +} + +export function tokenSecondsRemaining(token: string, offsetSeconds = 0): number { + const expDate = getTokenExpirationDate(token); + if (!expDate) { + return 0; + } + + const msRemaining = expDate.getTime() - Date.now() - offsetSeconds * 1000; + return Math.floor(msRemaining / 1000); +} + +export function tokenNeedsRefresh(token: string, minutesBeforeExpiration = 5): boolean { + const secondsRemaining = tokenSecondsRemaining(token); + return secondsRemaining < minutesBeforeExpiration * 60; +}