mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-26 01:13:14 +00:00
continue removing jslib code
This commit is contained in:
745
jslib-removal-plan.md
Normal file
745
jslib-removal-plan.md
Normal file
@@ -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<void>;
|
||||
getToken(): Promise<string | null>;
|
||||
getRefreshToken(): Promise<string | null>;
|
||||
clearToken(): Promise<void>;
|
||||
|
||||
// API key authentication
|
||||
getClientId(): Promise<string | null>;
|
||||
getClientSecret(): Promise<string | null>;
|
||||
|
||||
// Two-factor token (rarely used)
|
||||
getTwoFactorToken(): Promise<string | null>;
|
||||
clearTwoFactorToken(): Promise<void>;
|
||||
|
||||
// Token validation (delegates to jwt.util)
|
||||
decodeToken(token?: string): Promise<DecodedToken | null>;
|
||||
tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TOKEN_KEY);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearToken(): Promise<void> {
|
||||
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<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_ID_KEY);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_SECRET_KEY);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<void> {
|
||||
await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async decodeToken(token?: string): Promise<DecodedToken | null> {
|
||||
const tokenToUse = token ?? (await this.getToken());
|
||||
if (!tokenToUse) return null;
|
||||
|
||||
try {
|
||||
return decodeJwt(tokenToUse);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise<boolean> {
|
||||
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<void>;
|
||||
setUrlsFromStorage(): Promise<void>;
|
||||
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
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<EnvironmentUrls | null>;
|
||||
setEnvironmentUrls(urls: EnvironmentUrls): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
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<any> {
|
||||
return await this.stateService.getWindowSettings();
|
||||
}
|
||||
|
||||
async setWindowSettings(settings: any): Promise<void> {
|
||||
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<boolean> {
|
||||
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.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<string>,
|
||||
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 {}
|
||||
@@ -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>();
|
||||
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>();
|
||||
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<SymmetricCryptoKey>();
|
||||
|
||||
const cryptoService = Substitute.for<CryptoService>();
|
||||
cryptoService.getOrgKey(null).resolves(null);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
|
||||
|
||||
await encString.decrypt(null, key);
|
||||
|
||||
cryptoService.received().decryptToUtf8(encString, key);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<StorageService>;
|
||||
let secureStorageService: SubstituteOf<StorageService>;
|
||||
let stateFactory: SubstituteOf<StateFactory>;
|
||||
|
||||
let stateMigrationService: StateMigrationService;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = Substitute.for<StorageService>();
|
||||
secureStorageService = Substitute.for<StorageService>();
|
||||
stateFactory = Substitute.for<StateFactory>();
|
||||
|
||||
stateMigrationService = new StateMigrationService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
stateFactory,
|
||||
);
|
||||
});
|
||||
|
||||
describe("StateVersion 3 to 4 migration", async () => {
|
||||
beforeEach(() => {
|
||||
const globalVersion3: Partial<GlobalState> = {
|
||||
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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<T, K extends keyof T = keyof T>(
|
||||
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
|
||||
}
|
||||
|
||||
export function mockEnc(s: string): EncString {
|
||||
const mock = Substitute.for<EncString>();
|
||||
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++) {
|
||||
|
||||
@@ -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<any>;
|
||||
setKeyHash: (keyHash: string) => Promise<void>;
|
||||
setEncKey: (encKey: string) => Promise<void>;
|
||||
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
|
||||
setOrgKeys: (
|
||||
orgs: ProfileOrganizationResponse[],
|
||||
providerOrgs: ProfileProviderOrganizationResponse[],
|
||||
) => Promise<void>;
|
||||
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
|
||||
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyHash: () => Promise<string>;
|
||||
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||
getPublicKey: () => Promise<ArrayBuffer>;
|
||||
getPrivateKey: () => Promise<ArrayBuffer>;
|
||||
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
||||
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
||||
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
||||
hasKey: () => Promise<boolean>;
|
||||
hasKeyInMemory: (userId?: string) => Promise<boolean>;
|
||||
hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<boolean>;
|
||||
hasEncKey: () => Promise<boolean>;
|
||||
clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise<any>;
|
||||
clearKeyHash: () => Promise<any>;
|
||||
clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearProviderKeys: (memoryOnly?: boolean) => Promise<any>;
|
||||
clearPinProtectedKey: () => Promise<any>;
|
||||
clearKeys: (userId?: string) => Promise<any>;
|
||||
toggleKey: () => Promise<any>;
|
||||
makeKey: (
|
||||
password: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeKeyFromPin: (
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
protectedKeyCs?: EncString,
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
|
||||
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
|
||||
makePinKey: (
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
|
||||
hashPassword: (
|
||||
password: string,
|
||||
key: SymmetricCryptoKey,
|
||||
hashPurpose?: HashPurpose,
|
||||
) => Promise<string>;
|
||||
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
remakeEncKey: (
|
||||
key: SymmetricCryptoKey,
|
||||
encKey?: SymmetricCryptoKey,
|
||||
) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncString>;
|
||||
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
|
||||
rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise<EncString>;
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
|
||||
decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
randomNumber: (min: number, max: number) => Promise<number>;
|
||||
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
}
|
||||
@@ -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<Urls>;
|
||||
|
||||
hasBaseUrl: () => boolean;
|
||||
getNotificationsUrl: () => string;
|
||||
getWebVaultUrl: () => string;
|
||||
getSendUrl: () => string;
|
||||
getIconsUrl: () => string;
|
||||
getApiUrl: () => string;
|
||||
getIdentityUrl: () => string;
|
||||
getEventsUrl: () => string;
|
||||
getKeyConnectorUrl: () => string;
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
getUrls: () => Urls;
|
||||
}
|
||||
// Stub file - re-exports DC EnvironmentService
|
||||
export { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service";
|
||||
|
||||
@@ -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<T extends Account = Account> {
|
||||
accounts$: Observable<{ [userId: string]: T }>;
|
||||
activeAccount$: Observable<string>;
|
||||
|
||||
addAccount: (account: T) => Promise<void>;
|
||||
setActiveUser: (userId: string) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<void>;
|
||||
init: () => Promise<void>;
|
||||
|
||||
getAccessToken: (options?: StorageOptions) => Promise<string>;
|
||||
setAccessToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getAddEditCipherInfo: (options?: StorageOptions) => Promise<any>;
|
||||
setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
|
||||
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getApiKeyClientId: (options?: StorageOptions) => Promise<string>;
|
||||
setApiKeyClientId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getApiKeyClientSecret: (options?: StorageOptions) => Promise<string>;
|
||||
setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise<boolean>;
|
||||
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricLocked: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricLocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricText: (options?: StorageOptions) => Promise<string>;
|
||||
setBiometricText: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricUnlock: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
|
||||
getClearClipboard: (options?: StorageOptions) => Promise<number>;
|
||||
setClearClipboard: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getCollapsedGroupings: (options?: StorageOptions) => Promise<string[]>;
|
||||
setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise<void>;
|
||||
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
|
||||
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyB64: (options?: StorageOptions) => Promise<string>;
|
||||
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
|
||||
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getDecodedToken: (options?: StorageOptions) => Promise<any>;
|
||||
setDecodedToken: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setDecryptedCryptoSymmetricKey: (
|
||||
value: SymmetricCryptoKey,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDecryptedOrganizationKeys: (
|
||||
options?: StorageOptions,
|
||||
) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
setDecryptedOrganizationKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
|
||||
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
|
||||
setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedProviderKeys: (options?: StorageOptions) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
setDecryptedProviderKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
|
||||
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
||||
getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableAutoTotpCopy: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableBadgeCounter: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableGa: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableAlwaysOnTop: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableMinimizeToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableStartToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise<any>;
|
||||
setEncryptedOrganizationKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedPrivateKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedProviderKeys: (options?: StorageOptions) => Promise<any>;
|
||||
setEncryptedProviderKeys: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getEntityId: (options?: StorageOptions) => Promise<string>;
|
||||
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
||||
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
||||
getEquivalentDomains: (options?: StorageOptions) => Promise<any>;
|
||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getForcePasswordReset: (options?: StorageOptions) => Promise<boolean>;
|
||||
setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getInstalledVersion: (options?: StorageOptions) => Promise<string>;
|
||||
setInstalledVersion: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
||||
getKdfIterations: (options?: StorageOptions) => Promise<number>;
|
||||
setKdfIterations: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getKdfType: (options?: StorageOptions) => Promise<KdfType>;
|
||||
setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>;
|
||||
getKeyHash: (options?: StorageOptions) => Promise<string>;
|
||||
setKeyHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastActive: (options?: StorageOptions) => Promise<number>;
|
||||
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLegacyEtmKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setLegacyEtmKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
|
||||
getLocalData: (options?: StorageOptions) => Promise<any>;
|
||||
setLocalData: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLocale: (options?: StorageOptions) => Promise<string>;
|
||||
setLocale: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLoginRedirect: (options?: StorageOptions) => Promise<any>;
|
||||
setLoginRedirect: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getMainWindowSize: (options?: StorageOptions) => Promise<number>;
|
||||
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>;
|
||||
setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise<void>;
|
||||
getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise<boolean>;
|
||||
setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise<string>;
|
||||
setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
|
||||
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
|
||||
setOrganizations: (
|
||||
value: { [id: string]: OrganizationData },
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getUsernameGenerationOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getGeneratorOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setGeneratorOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getProtectedPin: (options?: StorageOptions) => Promise<string>;
|
||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
|
||||
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
|
||||
getPublicKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
|
||||
setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
|
||||
getRefreshToken: (options?: StorageOptions) => Promise<string>;
|
||||
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSettings: (options?: StorageOptions) => Promise<any>;
|
||||
setSettings: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoCodeVerifier: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoOrgIdentifier: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoState: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||
getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getStateVersion: () => Promise<number>;
|
||||
setStateVersion: (value: number) => Promise<void>;
|
||||
getWindow: () => Promise<WindowState>;
|
||||
setWindow: (value: WindowState) => Promise<void>;
|
||||
}
|
||||
// Stub file - re-exports DC StateService
|
||||
export { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export abstract class StateMigrationService {
|
||||
needsMigration: () => Promise<boolean>;
|
||||
migrate: () => Promise<void>;
|
||||
}
|
||||
@@ -1,32 +1,2 @@
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export abstract class TokenService {
|
||||
setTokens: (
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret: [string, string],
|
||||
) => Promise<any>;
|
||||
setToken: (token: string) => Promise<any>;
|
||||
getToken: () => Promise<string>;
|
||||
setRefreshToken: (refreshToken: string) => Promise<any>;
|
||||
getRefreshToken: () => Promise<string>;
|
||||
setClientId: (clientId: string) => Promise<any>;
|
||||
getClientId: () => Promise<string>;
|
||||
setClientSecret: (clientSecret: string) => Promise<any>;
|
||||
getClientSecret: () => Promise<string>;
|
||||
setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise<any>;
|
||||
getTwoFactorToken: () => Promise<string>;
|
||||
clearTwoFactorToken: () => Promise<any>;
|
||||
clearToken: (userId?: string) => Promise<any>;
|
||||
decodeToken: (token?: string) => any;
|
||||
getTokenExpirationDate: () => Promise<Date>;
|
||||
tokenSecondsRemaining: (offsetSeconds?: number) => Promise<number>;
|
||||
tokenNeedsRefresh: (minutes?: number) => Promise<boolean>;
|
||||
getUserId: () => Promise<string>;
|
||||
getEmail: () => Promise<string>;
|
||||
getEmailVerified: () => Promise<boolean>;
|
||||
getName: () => Promise<string>;
|
||||
getPremium: () => Promise<boolean>;
|
||||
getIssuer: () => Promise<string>;
|
||||
getIsExternal: () => Promise<boolean>;
|
||||
}
|
||||
// Stub file - re-exports DC TokenService
|
||||
export { TokenService } from "@/src/abstractions/token.service";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Account } from "../models/domain/account";
|
||||
|
||||
export class AccountFactory<T extends Account = Account> {
|
||||
private accountConstructor: new (init: Partial<T>) => T;
|
||||
|
||||
constructor(accountConstructor: new (init: Partial<T>) => T) {
|
||||
this.accountConstructor = accountConstructor;
|
||||
}
|
||||
|
||||
create(args: Partial<T>) {
|
||||
return new this.accountConstructor(args);
|
||||
}
|
||||
}
|
||||
@@ -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<TGlobal>;
|
||||
private accountFactory: AccountFactory<TAccount>;
|
||||
|
||||
constructor(
|
||||
globalStateConstructor: new (init: Partial<TGlobal>) => TGlobal,
|
||||
accountConstructor: new (init: Partial<TAccount>) => TAccount,
|
||||
) {
|
||||
this.globalStateFactory = new GlobalStateFactory(globalStateConstructor);
|
||||
this.accountFactory = new AccountFactory(accountConstructor);
|
||||
}
|
||||
|
||||
createGlobal(args: Partial<TGlobal>): TGlobal {
|
||||
return this.globalStateFactory.create(args);
|
||||
}
|
||||
|
||||
createAccount(args: Partial<TAccount>): TAccount {
|
||||
return this.accountFactory.create(args);
|
||||
}
|
||||
}
|
||||
@@ -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<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
decrypted?: TDecrypted;
|
||||
}
|
||||
|
||||
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: { [id: string]: TEncrypted };
|
||||
decrypted?: TDecrypted[];
|
||||
}
|
||||
|
||||
export class AccountData {
|
||||
ciphers?: any = new DataEncryptionPair<any, any>();
|
||||
folders?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
|
||||
localData?: any;
|
||||
sends?: any = new DataEncryptionPair<any, any>();
|
||||
collections?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
|
||||
policies?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
|
||||
passwordGenerationHistory?: EncryptionPair<any[], any[]> = new EncryptionPair<any[], any[]>();
|
||||
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<string, SymmetricCryptoKey> = new EncryptionPair<
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
organizationKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
|
||||
any,
|
||||
Map<string, SymmetricCryptoKey>
|
||||
>();
|
||||
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
|
||||
any,
|
||||
Map<string, SymmetricCryptoKey>
|
||||
>();
|
||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||
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<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
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<Account>) {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { View } from "../view/view";
|
||||
|
||||
import { EncString } from "./encString";
|
||||
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||
|
||||
export default class Domain {
|
||||
protected buildDomainModel<D extends Domain>(
|
||||
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<D extends Domain>(
|
||||
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<T extends View>(
|
||||
viewModel: T,
|
||||
map: any,
|
||||
orgId: string,
|
||||
key: SymmetricCryptoKey = null,
|
||||
): Promise<T> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<any> {
|
||||
await this.stateService.setCryptoMasterKey(key, { userId: userId });
|
||||
await this.storeKey(key, userId);
|
||||
}
|
||||
|
||||
async setKeyHash(keyHash: string): Promise<void> {
|
||||
await this.stateService.setKeyHash(keyHash);
|
||||
}
|
||||
|
||||
async setEncKey(encKey: string): Promise<void> {
|
||||
if (encKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(null);
|
||||
await this.stateService.setEncryptedCryptoSymmetricKey(encKey);
|
||||
}
|
||||
|
||||
async setEncPrivateKey(encPrivateKey: string): Promise<void> {
|
||||
if (encPrivateKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedPrivateKey(null);
|
||||
await this.stateService.setEncryptedPrivateKey(encPrivateKey);
|
||||
}
|
||||
|
||||
async setOrgKeys(
|
||||
orgs: ProfileOrganizationResponse[],
|
||||
providerOrgs: ProfileProviderOrganizationResponse[],
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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<string> {
|
||||
return await this.stateService.getKeyHash();
|
||||
}
|
||||
|
||||
async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise<boolean> {
|
||||
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<SymmetricCryptoKey> {
|
||||
return this.getEncKeyHelper(key);
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<ArrayBuffer> {
|
||||
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<ArrayBuffer> {
|
||||
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<string[]> {
|
||||
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<Map<string, SymmetricCryptoKey>> {
|
||||
const orgKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
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<SymmetricCryptoKey> {
|
||||
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<Map<string, SymmetricCryptoKey>> {
|
||||
const providerKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
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<SymmetricCryptoKey> {
|
||||
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<boolean> {
|
||||
return (
|
||||
(await this.hasKeyInMemory()) ||
|
||||
(await this.hasKeyStored(KeySuffixOptions.Auto)) ||
|
||||
(await this.hasKeyStored(KeySuffixOptions.Biometric))
|
||||
);
|
||||
}
|
||||
|
||||
async hasKeyInMemory(userId?: string): Promise<boolean> {
|
||||
return (await this.stateService.getCryptoMasterKey({ userId: userId })) != null;
|
||||
}
|
||||
|
||||
async hasKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
||||
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<boolean> {
|
||||
return (await this.stateService.getEncryptedCryptoSymmetricKey()) != null;
|
||||
}
|
||||
|
||||
async clearKey(clearSecretStorage = true, userId?: string): Promise<any> {
|
||||
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<any> {
|
||||
return await this.stateService.setKeyHash(null, { userId: userId });
|
||||
}
|
||||
|
||||
async clearEncKey(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedCryptoSymmetricKey(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise<any> {
|
||||
const keysToClear: Promise<void>[] = [
|
||||
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<void> {
|
||||
await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedProviderKeys(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedProviderKeys(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearPinProtectedKey(userId?: string): Promise<any> {
|
||||
return await this.stateService.setEncryptedPinProtected(null, { userId: userId });
|
||||
}
|
||||
|
||||
async clearKeys(userId?: string): Promise<any> {
|
||||
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<any> {
|
||||
const key = await this.getKey();
|
||||
|
||||
await this.setKey(key);
|
||||
}
|
||||
|
||||
async makeKey(
|
||||
password: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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<SymmetricCryptoKey> {
|
||||
const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations);
|
||||
return await this.stretchKey(pinKey);
|
||||
}
|
||||
|
||||
async makeSendKey(keyMaterial: ArrayBuffer): Promise<SymmetricCryptoKey> {
|
||||
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<string> {
|
||||
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<EncString> {
|
||||
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<EncArrayBuffer> {
|
||||
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<EncString> {
|
||||
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<ArrayBuffer> {
|
||||
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<ArrayBuffer> {
|
||||
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<string> {
|
||||
return await this.aesDecryptToUtf8(
|
||||
encString.encryptionType,
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key,
|
||||
);
|
||||
}
|
||||
|
||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
if (encBuf == null) {
|
||||
throw new Error("no encBuf.");
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(encBuf);
|
||||
const encType = encBytes[0];
|
||||
let ctBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let ivBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let macBytes: Uint8Array<ArrayBuffer> = 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<number> {
|
||||
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<EncryptedObject> {
|
||||
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<string> {
|
||||
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<ArrayBuffer> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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<void> {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
|
||||
private async getEncKeyHelper(key: SymmetricCryptoKey = null): Promise<SymmetricCryptoKey> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
urls: Observable<Urls> = 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<void> {
|
||||
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> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<TGlobalState, TAccount>,
|
||||
) {}
|
||||
|
||||
async needsMigration(): Promise<boolean> {
|
||||
const currentStateVersion = await this.getCurrentStateVersion();
|
||||
return currentStateVersion == null || currentStateVersion < StateVersion.Latest;
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
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<void> {
|
||||
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<GlobalState>(keys.global)) ?? this.stateFactory.createGlobal(null);
|
||||
globals.stateVersion = StateVersion.Two;
|
||||
globals.environmentUrls =
|
||||
(await this.get<EnvironmentUrls>(v1Keys.environmentUrls)) ?? globals.environmentUrls;
|
||||
globals.locale = (await this.get<string>(v1Keys.locale)) ?? globals.locale;
|
||||
globals.noAutoPromptBiometrics =
|
||||
(await this.get<boolean>(v1Keys.disableAutoBiometricsPrompt)) ??
|
||||
globals.noAutoPromptBiometrics;
|
||||
globals.noAutoPromptBiometricsText =
|
||||
(await this.get<string>(v1Keys.noAutoPromptBiometricsText)) ??
|
||||
globals.noAutoPromptBiometricsText;
|
||||
globals.ssoCodeVerifier =
|
||||
(await this.get<string>(v1Keys.ssoCodeVerifier)) ?? globals.ssoCodeVerifier;
|
||||
globals.ssoOrganizationIdentifier =
|
||||
(await this.get<string>(v1Keys.ssoIdentifier)) ?? globals.ssoOrganizationIdentifier;
|
||||
globals.ssoState = (await this.get<any>(v1Keys.ssoState)) ?? globals.ssoState;
|
||||
globals.rememberedEmail =
|
||||
(await this.get<string>(v1Keys.rememberedEmail)) ?? globals.rememberedEmail;
|
||||
globals.theme = (await this.get<ThemeType>(v1Keys.theme)) ?? globals.theme;
|
||||
globals.vaultTimeout = (await this.get<number>(v1Keys.vaultTimeout)) ?? globals.vaultTimeout;
|
||||
globals.vaultTimeoutAction =
|
||||
(await this.get<string>(v1Keys.vaultTimeoutAction)) ?? globals.vaultTimeoutAction;
|
||||
globals.window = (await this.get<any>(v1Keys.mainWindowSize)) ?? globals.window;
|
||||
globals.enableTray = (await this.get<boolean>(v1Keys.enableTray)) ?? globals.enableTray;
|
||||
globals.enableMinimizeToTray =
|
||||
(await this.get<boolean>(v1Keys.enableMinimizeToTray)) ?? globals.enableMinimizeToTray;
|
||||
globals.enableCloseToTray =
|
||||
(await this.get<boolean>(v1Keys.enableCloseToTray)) ?? globals.enableCloseToTray;
|
||||
globals.enableStartToTray =
|
||||
(await this.get<boolean>(v1Keys.enableStartToTray)) ?? globals.enableStartToTray;
|
||||
globals.openAtLogin = (await this.get<boolean>(v1Keys.openAtLogin)) ?? globals.openAtLogin;
|
||||
globals.alwaysShowDock =
|
||||
(await this.get<boolean>(v1Keys.alwaysShowDock)) ?? globals.alwaysShowDock;
|
||||
globals.enableBrowserIntegration =
|
||||
(await this.get<boolean>(v1Keys.enableBrowserIntegration)) ??
|
||||
globals.enableBrowserIntegration;
|
||||
globals.enableBrowserIntegrationFingerprint =
|
||||
(await this.get<boolean>(v1Keys.enableBrowserIntegrationFingerprint)) ??
|
||||
globals.enableBrowserIntegrationFingerprint;
|
||||
|
||||
const userId =
|
||||
(await this.get<string>(v1Keys.userId)) ?? (await this.get<string>(v1Keys.entityId));
|
||||
|
||||
const defaultAccount = this.stateFactory.createAccount(null);
|
||||
const accountSettings: AccountSettings = {
|
||||
autoConfirmFingerPrints:
|
||||
(await this.get<boolean>(v1Keys.autoConfirmFingerprints)) ??
|
||||
defaultAccount.settings.autoConfirmFingerPrints,
|
||||
autoFillOnPageLoadDefault:
|
||||
(await this.get<boolean>(v1Keys.autoFillOnPageLoadDefault)) ??
|
||||
defaultAccount.settings.autoFillOnPageLoadDefault,
|
||||
biometricLocked: null,
|
||||
biometricUnlock:
|
||||
(await this.get<boolean>(v1Keys.biometricUnlock)) ??
|
||||
defaultAccount.settings.biometricUnlock,
|
||||
clearClipboard:
|
||||
(await this.get<number>(v1Keys.clearClipboard)) ?? defaultAccount.settings.clearClipboard,
|
||||
defaultUriMatch:
|
||||
(await this.get<any>(v1Keys.defaultUriMatch)) ?? defaultAccount.settings.defaultUriMatch,
|
||||
disableAddLoginNotification:
|
||||
(await this.get<boolean>(v1Keys.disableAddLoginNotification)) ??
|
||||
defaultAccount.settings.disableAddLoginNotification,
|
||||
disableAutoBiometricsPrompt:
|
||||
(await this.get<boolean>(v1Keys.disableAutoBiometricsPrompt)) ??
|
||||
defaultAccount.settings.disableAutoBiometricsPrompt,
|
||||
disableAutoTotpCopy:
|
||||
(await this.get<boolean>(v1Keys.disableAutoTotpCopy)) ??
|
||||
defaultAccount.settings.disableAutoTotpCopy,
|
||||
disableBadgeCounter:
|
||||
(await this.get<boolean>(v1Keys.disableBadgeCounter)) ??
|
||||
defaultAccount.settings.disableBadgeCounter,
|
||||
disableChangedPasswordNotification:
|
||||
(await this.get<boolean>(v1Keys.disableChangedPasswordNotification)) ??
|
||||
defaultAccount.settings.disableChangedPasswordNotification,
|
||||
disableContextMenuItem:
|
||||
(await this.get<boolean>(v1Keys.disableContextMenuItem)) ??
|
||||
defaultAccount.settings.disableContextMenuItem,
|
||||
disableGa: (await this.get<boolean>(v1Keys.disableGa)) ?? defaultAccount.settings.disableGa,
|
||||
dontShowCardsCurrentTab:
|
||||
(await this.get<boolean>(v1Keys.dontShowCardsCurrentTab)) ??
|
||||
defaultAccount.settings.dontShowCardsCurrentTab,
|
||||
dontShowIdentitiesCurrentTab:
|
||||
(await this.get<boolean>(v1Keys.dontShowIdentitiesCurrentTab)) ??
|
||||
defaultAccount.settings.dontShowIdentitiesCurrentTab,
|
||||
enableAlwaysOnTop:
|
||||
(await this.get<boolean>(v1Keys.enableAlwaysOnTop)) ??
|
||||
defaultAccount.settings.enableAlwaysOnTop,
|
||||
enableAutoFillOnPageLoad:
|
||||
(await this.get<boolean>(v1Keys.enableAutoFillOnPageLoad)) ??
|
||||
defaultAccount.settings.enableAutoFillOnPageLoad,
|
||||
enableBiometric:
|
||||
(await this.get<boolean>(v1Keys.enableBiometric)) ??
|
||||
defaultAccount.settings.enableBiometric,
|
||||
enableFullWidth:
|
||||
(await this.get<boolean>(v1Keys.enableFullWidth)) ??
|
||||
defaultAccount.settings.enableFullWidth,
|
||||
enableGravitars:
|
||||
(await this.get<boolean>(v1Keys.enableGravatars)) ??
|
||||
defaultAccount.settings.enableGravitars,
|
||||
environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls,
|
||||
equivalentDomains:
|
||||
(await this.get<any>(v1Keys.equivalentDomains)) ??
|
||||
defaultAccount.settings.equivalentDomains,
|
||||
minimizeOnCopyToClipboard:
|
||||
(await this.get<boolean>(v1Keys.minimizeOnCopyToClipboard)) ??
|
||||
defaultAccount.settings.minimizeOnCopyToClipboard,
|
||||
neverDomains:
|
||||
(await this.get<any>(v1Keys.neverDomains)) ?? defaultAccount.settings.neverDomains,
|
||||
passwordGenerationOptions:
|
||||
(await this.get<any>(v1Keys.passwordGenerationOptions)) ??
|
||||
defaultAccount.settings.passwordGenerationOptions,
|
||||
pinProtected: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<string>(v1Keys.pinProtected),
|
||||
},
|
||||
protectedPin: await this.get<string>(v1Keys.protectedPin),
|
||||
settings: userId == null ? null : await this.get<any>(v1KeyPrefixes.settings + userId),
|
||||
vaultTimeout:
|
||||
(await this.get<number>(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout,
|
||||
vaultTimeoutAction:
|
||||
(await this.get<string>(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<string>(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<any[]>(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<any[]>(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<string>(v1Keys.clientSecret),
|
||||
cryptoMasterKey: null,
|
||||
cryptoMasterKeyAuto: null,
|
||||
cryptoMasterKeyB64: null,
|
||||
cryptoMasterKeyBiometric: null,
|
||||
cryptoSymmetricKey: {
|
||||
encrypted: await this.get<string>(v1Keys.encKey),
|
||||
decrypted: null,
|
||||
},
|
||||
legacyEtmKey: null,
|
||||
organizationKeys: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any>(v1Keys.encOrgKeys),
|
||||
},
|
||||
privateKey: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<string>(v1Keys.encPrivate),
|
||||
},
|
||||
providerKeys: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any>(v1Keys.encProviderKeys),
|
||||
},
|
||||
publicKey: null,
|
||||
},
|
||||
profile: {
|
||||
apiKeyClientId: await this.get<string>(v1Keys.clientId),
|
||||
authenticationStatus: null,
|
||||
convertAccountToKeyConnector: await this.get<boolean>(v1Keys.convertAccountToKeyConnector),
|
||||
email: await this.get<string>(v1Keys.userEmail),
|
||||
emailVerified: await this.get<boolean>(v1Keys.emailVerified),
|
||||
entityId: null,
|
||||
entityType: null,
|
||||
everBeenUnlocked: null,
|
||||
forcePasswordReset: null,
|
||||
hasPremiumPersonally: null,
|
||||
kdfIterations: await this.get<number>(v1Keys.kdfIterations),
|
||||
kdfType: await this.get<KdfType>(v1Keys.kdf),
|
||||
keyHash: await this.get<string>(v1Keys.keyHash),
|
||||
lastSync: null,
|
||||
userId: userId,
|
||||
usesKeyConnector: null,
|
||||
},
|
||||
settings: accountSettings,
|
||||
tokens: {
|
||||
accessToken: await this.get<string>(v1Keys.accessToken),
|
||||
decodedToken: null,
|
||||
refreshToken: await this.get<string>(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<number>(v1Keys.lastActive),
|
||||
};
|
||||
accountActivity[userId] = await this.get<number>(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<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<TAccount>(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<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<TAccount>(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<T>(key: string): Promise<T> {
|
||||
return this.storageService.get<T>(key, this.options);
|
||||
}
|
||||
|
||||
protected set(key: string, value: any): Promise<any> {
|
||||
if (value == null) {
|
||||
return this.storageService.remove(key, this.options);
|
||||
}
|
||||
return this.storageService.save(key, value, this.options);
|
||||
}
|
||||
|
||||
protected async getGlobals(): Promise<TGlobalState> {
|
||||
return await this.get<TGlobalState>(keys.global);
|
||||
}
|
||||
|
||||
protected async getCurrentStateVersion(): Promise<StateVersion> {
|
||||
return (await this.getGlobals())?.stateVersion ?? StateVersion.One;
|
||||
}
|
||||
}
|
||||
@@ -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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
return await this.stateService.setApiKeyClientId(clientId);
|
||||
}
|
||||
|
||||
async getClientId(): Promise<string> {
|
||||
return await this.stateService.getApiKeyClientId();
|
||||
}
|
||||
|
||||
async setClientSecret(clientSecret: string): Promise<any> {
|
||||
return await this.stateService.setApiKeyClientSecret(clientSecret);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string> {
|
||||
return await this.stateService.getApiKeyClientSecret();
|
||||
}
|
||||
|
||||
async setToken(token: string): Promise<void> {
|
||||
await this.stateService.setAccessToken(token);
|
||||
}
|
||||
|
||||
async getToken(): Promise<string> {
|
||||
return await this.stateService.getAccessToken();
|
||||
}
|
||||
|
||||
async setRefreshToken(refreshToken: string): Promise<any> {
|
||||
return await this.stateService.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string> {
|
||||
return await this.stateService.getRefreshToken();
|
||||
}
|
||||
|
||||
async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string> {
|
||||
return await this.stateService.getTwoFactorToken();
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(null);
|
||||
}
|
||||
|
||||
async clearToken(userId?: string): Promise<any> {
|
||||
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<any> {
|
||||
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<Date> {
|
||||
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<number> {
|
||||
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<boolean> {
|
||||
const sRemaining = await this.tokenSecondsRemaining();
|
||||
return sRemaining < 60 * minutes;
|
||||
}
|
||||
|
||||
async getUserId(): Promise<string> {
|
||||
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<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.email === "undefined") {
|
||||
throw new Error("No email found");
|
||||
}
|
||||
|
||||
return decoded.email as string;
|
||||
}
|
||||
|
||||
async getEmailVerified(): Promise<boolean> {
|
||||
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<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.name === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decoded.name as string;
|
||||
}
|
||||
|
||||
async getPremium(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.premium === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return decoded.premium as boolean;
|
||||
}
|
||||
|
||||
async getIssuer(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.iss === "undefined") {
|
||||
throw new Error("No issuer found");
|
||||
}
|
||||
|
||||
return decoded.iss as string;
|
||||
}
|
||||
|
||||
async getIsExternal(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
|
||||
return Array.isArray(decoded.amr) && decoded.amr.includes("external");
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
return await this.stateService.getEnableBiometric();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
src/abstractions/environment.service.ts
Normal file
13
src/abstractions/environment.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
|
||||
export { EnvironmentUrls };
|
||||
|
||||
export abstract class EnvironmentService {
|
||||
abstract setUrls(urls: EnvironmentUrls): Promise<void>;
|
||||
abstract setUrlsFromStorage(): Promise<void>;
|
||||
|
||||
abstract hasBaseUrl(): boolean;
|
||||
abstract getApiUrl(): string;
|
||||
abstract getIdentityUrl(): string;
|
||||
abstract getWebVaultUrl(): string;
|
||||
}
|
||||
@@ -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<IConfiguration>(type: DirectoryType): Promise<IConfiguration>;
|
||||
abstract setDirectory(
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
| GSuiteConfiguration
|
||||
| EntraIdConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration,
|
||||
): Promise<any>;
|
||||
abstract getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration>;
|
||||
abstract setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration>;
|
||||
abstract setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration>;
|
||||
abstract setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration>;
|
||||
abstract setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration>;
|
||||
abstract setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getOrganizationId(options?: StorageOptions): Promise<string>;
|
||||
abstract setOrganizationId(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getSync(options?: StorageOptions): Promise<SyncConfiguration>;
|
||||
abstract setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getDirectoryType(options?: StorageOptions): Promise<DirectoryType>;
|
||||
abstract setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void>;
|
||||
abstract getUserDelta(options?: StorageOptions): Promise<string>;
|
||||
abstract setUserDelta(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastUserSync(options?: StorageOptions): Promise<Date>;
|
||||
abstract setLastUserSync(value: Date, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastGroupSync(options?: StorageOptions): Promise<Date>;
|
||||
abstract setLastGroupSync(value: Date, options?: StorageOptions): Promise<void>;
|
||||
abstract getGroupDelta(options?: StorageOptions): Promise<string>;
|
||||
abstract setGroupDelta(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastSyncHash(options?: StorageOptions): Promise<string>;
|
||||
abstract setLastSyncHash(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getSyncingDir(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setSyncingDir(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract clearSyncSettings(syncHashToo: boolean): Promise<void>;
|
||||
|
||||
// Window settings (for WindowMain)
|
||||
abstract getWindow(options?: StorageOptions): Promise<any>;
|
||||
abstract setWindow(value: any, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableAlwaysOnTop(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Tray settings (for TrayMain)
|
||||
abstract getEnableTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableCloseToTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getAlwaysShowDock(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Environment URLs (adding convenience methods)
|
||||
abstract getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls>;
|
||||
abstract setEnvironmentUrls(value: EnvironmentUrls): Promise<void>;
|
||||
abstract getApiUrl(options?: StorageOptions): Promise<string>;
|
||||
abstract getIdentityUrl(options?: StorageOptions): Promise<string>;
|
||||
|
||||
// Token management (replaces TokenService.clearToken())
|
||||
abstract clearAuthTokens(): Promise<void>;
|
||||
}
|
||||
@@ -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<Account> {
|
||||
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
|
||||
setDirectory: (
|
||||
export abstract class StateService {
|
||||
abstract getDirectory<IConfiguration>(type: DirectoryType): Promise<IConfiguration>;
|
||||
abstract setDirectory(
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
@@ -20,37 +19,89 @@ export abstract class StateService extends BaseStateServiceAbstraction<Account>
|
||||
| EntraIdConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration,
|
||||
) => Promise<any>;
|
||||
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
|
||||
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
|
||||
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getEntraConfiguration: (options?: StorageOptions) => Promise<EntraIdConfiguration>;
|
||||
setEntraConfiguration: (value: EntraIdConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
|
||||
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
|
||||
setOneLoginConfiguration: (
|
||||
): Promise<any>;
|
||||
abstract getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration>;
|
||||
abstract setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration>;
|
||||
abstract setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration>;
|
||||
abstract setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration>;
|
||||
abstract setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration>;
|
||||
abstract setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getOrganizationId: (options?: StorageOptions) => Promise<string>;
|
||||
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
|
||||
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
|
||||
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
|
||||
getUserDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getGroupDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
|
||||
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
|
||||
): Promise<void>;
|
||||
abstract getOrganizationId(options?: StorageOptions): Promise<string>;
|
||||
abstract setOrganizationId(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getSync(options?: StorageOptions): Promise<SyncConfiguration>;
|
||||
abstract setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getDirectoryType(options?: StorageOptions): Promise<DirectoryType>;
|
||||
abstract setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void>;
|
||||
abstract getUserDelta(options?: StorageOptions): Promise<string>;
|
||||
abstract setUserDelta(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastUserSync(options?: StorageOptions): Promise<Date>;
|
||||
abstract setLastUserSync(value: Date, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastGroupSync(options?: StorageOptions): Promise<Date>;
|
||||
abstract setLastGroupSync(value: Date, options?: StorageOptions): Promise<void>;
|
||||
abstract getGroupDelta(options?: StorageOptions): Promise<string>;
|
||||
abstract setGroupDelta(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastSyncHash(options?: StorageOptions): Promise<string>;
|
||||
abstract setLastSyncHash(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getSyncingDir(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setSyncingDir(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract clearSyncSettings(syncHashToo: boolean): Promise<void>;
|
||||
|
||||
// Window settings (for WindowMain)
|
||||
abstract getWindow(options?: StorageOptions): Promise<any>;
|
||||
abstract setWindow(value: any, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableAlwaysOnTop(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Tray settings (for TrayMain)
|
||||
abstract getEnableTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableCloseToTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getAlwaysShowDock(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Environment URLs (adding convenience methods)
|
||||
abstract getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls>;
|
||||
abstract setEnvironmentUrls(value: EnvironmentUrls): Promise<void>;
|
||||
abstract getApiUrl(options?: StorageOptions): Promise<string>;
|
||||
abstract getIdentityUrl(options?: StorageOptions): Promise<string>;
|
||||
|
||||
// Token management (replaces TokenService.clearToken())
|
||||
abstract clearAuthTokens(): Promise<void>;
|
||||
abstract getAccessToken(options?: StorageOptions): Promise<string>;
|
||||
abstract setAccessToken(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getRefreshToken(options?: StorageOptions): Promise<string>;
|
||||
abstract setRefreshToken(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getApiKeyClientId(options?: StorageOptions): Promise<string>;
|
||||
abstract setApiKeyClientId(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getApiKeyClientSecret(options?: StorageOptions): Promise<string>;
|
||||
abstract setApiKeyClientSecret(value: string, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Lifecycle methods
|
||||
abstract init(): Promise<void>;
|
||||
abstract clean(options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Additional state methods
|
||||
abstract getLocale(options?: StorageOptions): Promise<string>;
|
||||
abstract setLocale(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getInstalledVersion(options?: StorageOptions): Promise<string>;
|
||||
abstract setInstalledVersion(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getIsAuthenticated(options?: StorageOptions): Promise<boolean>;
|
||||
abstract getEntityId(options?: StorageOptions): Promise<string>;
|
||||
abstract setEntityId(value: string, options?: StorageOptions): Promise<void>;
|
||||
}
|
||||
|
||||
25
src/abstractions/token.service.ts
Normal file
25
src/abstractions/token.service.ts
Normal file
@@ -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<void>;
|
||||
abstract getToken(): Promise<string | null>;
|
||||
abstract getRefreshToken(): Promise<string | null>;
|
||||
abstract clearToken(): Promise<void>;
|
||||
|
||||
// API key authentication
|
||||
abstract getClientId(): Promise<string | null>;
|
||||
abstract getClientSecret(): Promise<string | null>;
|
||||
|
||||
// Two-factor token (rarely used)
|
||||
abstract getTwoFactorToken(): Promise<string | null>;
|
||||
abstract clearTwoFactorToken(): Promise<void>;
|
||||
|
||||
// Token validation (delegates to jwt.util)
|
||||
abstract decodeToken(token?: string): Promise<DecodedToken | null>;
|
||||
abstract tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise<boolean>;
|
||||
}
|
||||
@@ -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<void> {
|
||||
// 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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<void> {
|
||||
export function initFactory(injector: Injector): () => Promise<void> {
|
||||
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[],
|
||||
})
|
||||
|
||||
36
src/bwdc.ts
36
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Response> {
|
||||
@@ -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) {
|
||||
|
||||
25
src/main.ts
25
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,
|
||||
|
||||
@@ -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<Account>) {
|
||||
super(init);
|
||||
this.directoryConfigurations = init?.directoryConfigurations ?? new DirectoryConfigurations();
|
||||
this.directorySettings = init?.directorySettings ?? new DirectorySettings();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientKeys {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<LogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let stateServiceVNext: MockProxy<StateServiceVNext>;
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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<GSuiteConfiguration>(
|
||||
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>(
|
||||
DirectoryType.GSuite,
|
||||
);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncConfig = await this.stateServiceVNext.getSync();
|
||||
this.syncConfig = await this.stateService.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
78
src/services/environment/environment.service.ts
Normal file
78
src/services/environment/environment.service.ts
Normal file
@@ -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<void> {
|
||||
// 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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
if (await this.stateMigrationService.needsMigration()) {
|
||||
await this.stateMigrationService.migrate();
|
||||
}
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
// 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<T extends IConfiguration>(type: DirectoryType): Promise<T> {
|
||||
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<any> {
|
||||
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<IConfiguration> {
|
||||
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<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.ldap);
|
||||
}
|
||||
|
||||
private async setLdapSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.ldap);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.ldap, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getGsuiteSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.gsuite);
|
||||
}
|
||||
|
||||
private async setGsuiteSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.gsuite);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.gsuite, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntraSecret(): Promise<string> {
|
||||
// Try new key first, fall back to old azure key for backwards compatibility
|
||||
const entraKey = await this.secureStorageService.get<string>(SecureStorageKeys.entra);
|
||||
if (entraKey != null) {
|
||||
return entraKey;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.azure);
|
||||
}
|
||||
|
||||
private async setEntraSecret(value: string): Promise<void> {
|
||||
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<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.okta);
|
||||
}
|
||||
|
||||
private async setOktaSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.okta);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.okta, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOneLoginSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.oneLogin);
|
||||
}
|
||||
|
||||
private async setOneLoginSecret(value: string): Promise<void> {
|
||||
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<LdapConfiguration> {
|
||||
return await this.storageService.get<LdapConfiguration>(StorageKeys.directory_ldap);
|
||||
}
|
||||
|
||||
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_ldap, value);
|
||||
}
|
||||
|
||||
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
|
||||
return await this.storageService.get<GSuiteConfiguration>(StorageKeys.directory_gsuite);
|
||||
}
|
||||
|
||||
async setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_gsuite, value);
|
||||
}
|
||||
|
||||
async getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration> {
|
||||
return await this.storageService.get<EntraIdConfiguration>(StorageKeys.directory_entra);
|
||||
}
|
||||
|
||||
async setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_entra, value);
|
||||
}
|
||||
|
||||
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
|
||||
return await this.storageService.get<OktaConfiguration>(StorageKeys.directory_okta);
|
||||
}
|
||||
|
||||
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_okta, value);
|
||||
}
|
||||
|
||||
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
|
||||
return await this.storageService.get<OneLoginConfiguration>(StorageKeys.directory_onelogin);
|
||||
}
|
||||
|
||||
async setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.directory_onelogin, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Directory Settings Methods
|
||||
// ===================================================================
|
||||
|
||||
async getOrganizationId(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(StorageKeys.organizationId);
|
||||
}
|
||||
|
||||
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
|
||||
const currentId = await this.getOrganizationId();
|
||||
if (currentId !== value) {
|
||||
await this.clearSyncSettings();
|
||||
}
|
||||
await this.storageService.save(StorageKeys.organizationId, value);
|
||||
}
|
||||
|
||||
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
|
||||
return await this.storageService.get<SyncConfiguration>(StorageKeys.sync);
|
||||
}
|
||||
|
||||
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.sync, value);
|
||||
}
|
||||
|
||||
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
|
||||
return await this.storageService.get<DirectoryType>(StorageKeys.directoryType);
|
||||
}
|
||||
|
||||
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
|
||||
const currentType = await this.getDirectoryType();
|
||||
if (value !== currentType) {
|
||||
await this.clearSyncSettings();
|
||||
}
|
||||
await this.storageService.save(StorageKeys.directoryType, value);
|
||||
}
|
||||
|
||||
async getLastUserSync(options?: StorageOptions): Promise<Date> {
|
||||
const dateString = await this.storageService.get<string>(SecureStorageKeys.lastUserSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastUserSync, value);
|
||||
}
|
||||
|
||||
async getLastGroupSync(options?: StorageOptions): Promise<Date> {
|
||||
const dateString = await this.storageService.get<string>(SecureStorageKeys.lastGroupSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastGroupSync, value);
|
||||
}
|
||||
|
||||
async getLastSyncHash(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.lastSyncHash);
|
||||
}
|
||||
|
||||
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.lastSyncHash, value);
|
||||
}
|
||||
|
||||
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
|
||||
return await this.storageService.get<boolean>(StorageKeys.syncingDir);
|
||||
}
|
||||
|
||||
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.syncingDir, value);
|
||||
}
|
||||
|
||||
async getUserDelta(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.userDelta);
|
||||
}
|
||||
|
||||
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.userDelta, value);
|
||||
}
|
||||
|
||||
async getGroupDelta(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>(SecureStorageKeys.groupDelta);
|
||||
}
|
||||
|
||||
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(SecureStorageKeys.groupDelta, value);
|
||||
}
|
||||
|
||||
async clearSyncSettings(hashToo = false): Promise<void> {
|
||||
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<EnvironmentUrls> {
|
||||
return await this.storageService.get<EnvironmentUrls>(StorageKeys.environmentUrls);
|
||||
}
|
||||
|
||||
async setEnvironmentUrls(value: EnvironmentUrls): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.environmentUrls, value);
|
||||
}
|
||||
|
||||
async getApiUrl(options?: StorageOptions): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
return await this.storageService.get<string>("locale");
|
||||
}
|
||||
|
||||
async setLocale(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("locale", value);
|
||||
}
|
||||
|
||||
async getInstalledVersion(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("installedVersion");
|
||||
}
|
||||
|
||||
async setInstalledVersion(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("installedVersion", value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Window Settings (for WindowMain)
|
||||
// ===================================================================
|
||||
|
||||
async getWindow(options?: StorageOptions): Promise<any> {
|
||||
return await this.storageService.get(StorageKeys.window);
|
||||
}
|
||||
|
||||
async setWindow(value: any, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.window, value);
|
||||
}
|
||||
|
||||
async getEnableAlwaysOnTop(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableAlwaysOnTop)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableAlwaysOnTop, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Tray Settings (for TrayMain)
|
||||
// ===================================================================
|
||||
|
||||
async getEnableTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableTray)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableTray(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableTray, value);
|
||||
}
|
||||
|
||||
async getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableMinimizeToTray)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableMinimizeToTray, value);
|
||||
}
|
||||
|
||||
async getEnableCloseToTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableCloseToTray)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableCloseToTray, value);
|
||||
}
|
||||
|
||||
async getAlwaysShowDock(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.alwaysShowDock)) ?? false;
|
||||
}
|
||||
|
||||
async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.alwaysShowDock, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Token Management (replaces TokenService.clearToken())
|
||||
// ===================================================================
|
||||
|
||||
async clearAuthTokens(): Promise<void> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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<StorageService>;
|
||||
let secureStorageService: MockProxy<StorageService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateMigrationService: MockProxy<StateMigrationService>;
|
||||
let stateService: StateServiceVNextImplementation;
|
||||
let stateService: StateServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = mock<StorageService>();
|
||||
@@ -29,7 +29,7 @@ describe("StateServiceVNextImplementation", () => {
|
||||
logService = mock<LogService>();
|
||||
stateMigrationService = mock<StateMigrationService>();
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<GlobalState, Account>
|
||||
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<GlobalState, Account>,
|
||||
) {
|
||||
super(storageService, secureStorageService, logService, stateMigrationService, stateFactory);
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (await this.stateMigrationService.needsMigration()) {
|
||||
await this.stateMigrationService.migrate();
|
||||
}
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
// 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<T extends IConfiguration>(type: DirectoryType): Promise<T> {
|
||||
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<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}_${SecureStorageKeys.ldap}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async setLdapKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}_${SecureStorageKeys.gsuite}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async setGsuiteKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entraKey = await this.secureStorageService.get<string>(
|
||||
`${options.userId}_${SecureStorageKeys.entra}`,
|
||||
);
|
||||
|
||||
if (entraKey != null) {
|
||||
return entraKey;
|
||||
}
|
||||
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}_${SecureStorageKeys.azure}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async setEntraKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}_${SecureStorageKeys.okta}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async setOktaKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(
|
||||
`${options.userId}_${SecureStorageKeys.oneLogin}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async setOneLoginKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<IConfiguration> {
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
@@ -261,112 +157,135 @@ export class StateService
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Secret Storage Methods (Secure Storage)
|
||||
// ===================================================================
|
||||
|
||||
private async getLdapSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.ldap);
|
||||
}
|
||||
|
||||
private async setLdapSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.ldap);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.ldap, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getGsuiteSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.gsuite);
|
||||
}
|
||||
|
||||
private async setGsuiteSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.gsuite);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.gsuite, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntraSecret(): Promise<string> {
|
||||
// Try new key first, fall back to old azure key for backwards compatibility
|
||||
const entraKey = await this.secureStorageService.get<string>(SecureStorageKeys.entra);
|
||||
if (entraKey != null) {
|
||||
return entraKey;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.azure);
|
||||
}
|
||||
|
||||
private async setEntraSecret(value: string): Promise<void> {
|
||||
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<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.okta);
|
||||
}
|
||||
|
||||
private async setOktaSecret(value: string): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.okta);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.okta, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOneLoginSecret(): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.oneLogin);
|
||||
}
|
||||
|
||||
private async setOneLoginSecret(value: string): Promise<void> {
|
||||
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<LdapConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.ldap;
|
||||
return await this.storageService.get<LdapConfiguration>(StorageKeys.directory_ldap);
|
||||
}
|
||||
|
||||
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
|
||||
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<GSuiteConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.gsuite;
|
||||
return await this.storageService.get<GSuiteConfiguration>(StorageKeys.directory_gsuite);
|
||||
}
|
||||
|
||||
async setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
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<EntraIdConfiguration> {
|
||||
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<EntraIdConfiguration>(StorageKeys.directory_entra);
|
||||
}
|
||||
|
||||
async setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
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<OktaConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.okta;
|
||||
return await this.storageService.get<OktaConfiguration>(StorageKeys.directory_okta);
|
||||
}
|
||||
|
||||
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
|
||||
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<OneLoginConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.oneLogin;
|
||||
return await this.storageService.get<OneLoginConfiguration>(StorageKeys.directory_onelogin);
|
||||
}
|
||||
|
||||
async setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
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<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.organizationId;
|
||||
return await this.storageService.get<string>(StorageKeys.organizationId);
|
||||
}
|
||||
|
||||
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
|
||||
@@ -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<SyncConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.sync;
|
||||
return await this.storageService.get<SyncConfiguration>(StorageKeys.sync);
|
||||
}
|
||||
|
||||
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
|
||||
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<DirectoryType> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.directoryType;
|
||||
return await this.storageService.get<DirectoryType>(StorageKeys.directoryType);
|
||||
}
|
||||
|
||||
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
|
||||
@@ -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<Date> {
|
||||
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<string>(SecureStorageKeys.lastUserSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
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<Date> {
|
||||
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<string>(SecureStorageKeys.lastGroupSync);
|
||||
return dateString ? new Date(dateString) : null;
|
||||
}
|
||||
|
||||
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.lastSyncHash;
|
||||
return await this.storageService.get<string>(SecureStorageKeys.lastSyncHash);
|
||||
}
|
||||
|
||||
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<boolean> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
||||
?.directorySettings?.syncingDir;
|
||||
return await this.storageService.get<boolean>(StorageKeys.syncingDir);
|
||||
}
|
||||
|
||||
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.userDelta;
|
||||
return await this.storageService.get<string>(SecureStorageKeys.userDelta);
|
||||
}
|
||||
|
||||
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.groupDelta;
|
||||
return await this.storageService.get<string>(SecureStorageKeys.groupDelta);
|
||||
}
|
||||
|
||||
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.scaffoldNewAccountDiskStorage(account);
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountDiskStorage(account: Account): Promise<void> {
|
||||
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<any>(keys.tempAccountSettings);
|
||||
account.directorySettings = await this.storageService.get<any>(keys.tempDirectorySettings);
|
||||
account.directoryConfigurations = await this.storageService.get<any>(
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<EnvironmentUrls> {
|
||||
return this.getGlobalEnvironmentUrls(options);
|
||||
return await this.storageService.get<EnvironmentUrls>(StorageKeys.environmentUrls);
|
||||
}
|
||||
|
||||
async setEnvironmentUrls(value: EnvironmentUrls): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.environmentUrls, value);
|
||||
}
|
||||
|
||||
async getApiUrl(options?: StorageOptions): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
return await this.storageService.get<string>("locale");
|
||||
}
|
||||
|
||||
async setLocale(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("locale", value);
|
||||
}
|
||||
|
||||
async getInstalledVersion(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("installedVersion");
|
||||
}
|
||||
|
||||
async setInstalledVersion(value: string, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save("installedVersion", value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Window Settings (for WindowMain)
|
||||
// ===================================================================
|
||||
|
||||
async getWindow(options?: StorageOptions): Promise<any> {
|
||||
return await this.storageService.get(StorageKeys.window);
|
||||
}
|
||||
|
||||
async setWindow(value: any, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.window, value);
|
||||
}
|
||||
|
||||
async getEnableAlwaysOnTop(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableAlwaysOnTop)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableAlwaysOnTop, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Tray Settings (for TrayMain)
|
||||
// ===================================================================
|
||||
|
||||
async getEnableTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableTray)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableTray(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableTray, value);
|
||||
}
|
||||
|
||||
async getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableMinimizeToTray)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableMinimizeToTray, value);
|
||||
}
|
||||
|
||||
async getEnableCloseToTray(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.enableCloseToTray)) ?? false;
|
||||
}
|
||||
|
||||
async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.enableCloseToTray, value);
|
||||
}
|
||||
|
||||
async getAlwaysShowDock(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.storageService.get<boolean>(StorageKeys.alwaysShowDock)) ?? false;
|
||||
}
|
||||
|
||||
async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
await this.storageService.save(StorageKeys.alwaysShowDock, value);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Token Management (replaces TokenService.clearToken())
|
||||
// ===================================================================
|
||||
|
||||
async clearAuthTokens(): Promise<void> {
|
||||
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<string> {
|
||||
return await this.secureStorageService.get<string>("accessToken");
|
||||
}
|
||||
|
||||
async setAccessToken(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove("accessToken");
|
||||
} else {
|
||||
await this.secureStorageService.save("accessToken", value);
|
||||
}
|
||||
}
|
||||
|
||||
async getRefreshToken(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>("refreshToken");
|
||||
}
|
||||
|
||||
async setRefreshToken(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove("refreshToken");
|
||||
} else {
|
||||
await this.secureStorageService.save("refreshToken", value);
|
||||
}
|
||||
}
|
||||
|
||||
async getApiKeyClientId(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>("apiKeyClientId");
|
||||
}
|
||||
|
||||
async setApiKeyClientId(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove("apiKeyClientId");
|
||||
} else {
|
||||
await this.secureStorageService.save("apiKeyClientId", value);
|
||||
}
|
||||
}
|
||||
|
||||
async getApiKeyClientSecret(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>("apiKeyClientSecret");
|
||||
}
|
||||
|
||||
async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove("apiKeyClientSecret");
|
||||
} else {
|
||||
await this.secureStorageService.save("apiKeyClientSecret", value);
|
||||
}
|
||||
}
|
||||
|
||||
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
||||
// Check if access token exists
|
||||
const token = await this.getAccessToken(options);
|
||||
return token != null;
|
||||
}
|
||||
|
||||
async getEntityId(options?: StorageOptions): Promise<string> {
|
||||
return await this.storageService.get<string>("entityId");
|
||||
}
|
||||
|
||||
async setEntityId(value: string, options?: StorageOptions): Promise<void> {
|
||||
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";
|
||||
|
||||
@@ -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<boolean> {
|
||||
const currentStateVersion = await this.getCurrentStateVersion();
|
||||
return currentStateVersion == null || currentStateVersion < StateVersion.Latest;
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion = await this.getCurrentStateVersion();
|
||||
while (currentStateVersion < StateVersion.Latest) {
|
||||
@@ -56,14 +70,12 @@ export class StateMigrationService extends BaseStateMigrationService {
|
||||
}
|
||||
|
||||
protected async migrateStateFrom1To2(useSecureStorageForSecrets = true): Promise<void> {
|
||||
// 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<string>(Keys.entityId);
|
||||
const clientId = await this.get<string>(ClientKeys.clientId);
|
||||
const clientSecret = await this.get<string>(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 <T>(type: DirectoryType) =>
|
||||
await this.get<T>(SecureStorageKeys.directoryConfigPrefix + type);
|
||||
const directoryConfigs: DirectoryConfigurations = {
|
||||
@@ -104,16 +116,19 @@ export class StateMigrationService extends BaseStateMigrationService {
|
||||
groupDelta: await this.get<string>(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<Account>(userId);
|
||||
const account = await this.get<any>(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<void> {
|
||||
if (useSecureStorageForSecrets) {
|
||||
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
|
||||
|
||||
if (authenticatedUserIds && authenticatedUserIds.length > 0) {
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<any>(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<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
|
||||
|
||||
if (authenticatedUserIds && authenticatedUserIds.length > 0) {
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<Account>(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<any>(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<Account>(userId);
|
||||
const account = await this.get<any>(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<T>(key: string): Promise<T> {
|
||||
return this.storageService.get<T>(key, this.options);
|
||||
}
|
||||
|
||||
protected set(key: string, value: any): Promise<any> {
|
||||
if (value == null) {
|
||||
return this.storageService.remove(key, this.options);
|
||||
}
|
||||
return this.storageService.save(key, value, this.options);
|
||||
}
|
||||
|
||||
protected async getGlobals(): Promise<any> {
|
||||
return await this.get<any>(StateKeys.global);
|
||||
}
|
||||
|
||||
protected async getCurrentStateVersion(): Promise<StateVersion> {
|
||||
// Try new flat structure first
|
||||
const flatVersion = await this.get<StateVersion>(StorageKeysVNext.stateVersion);
|
||||
if (flatVersion != null) {
|
||||
return flatVersion;
|
||||
}
|
||||
|
||||
// Fall back to old globals structure
|
||||
const globals = await this.getGlobals();
|
||||
return globals?.stateVersion ?? StateVersion.One;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let stateServiceVNext: MockProxy<StateServiceVNext>;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ApiService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateServiceVNext: MockProxy<StateServiceVNext>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let directoryFactory: MockProxy<DirectoryFactoryService>;
|
||||
let batchRequestBuilder: MockProxy<BatchRequestBuilder>;
|
||||
@@ -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<LdapDirectoryService>();
|
||||
@@ -59,7 +56,6 @@ describe("SyncService", () => {
|
||||
apiService,
|
||||
messagingService,
|
||||
i18nService,
|
||||
stateServiceVNext,
|
||||
stateService,
|
||||
batchRequestBuilder,
|
||||
singleRequestBuilder,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
90
src/services/token/token.service.ts
Normal file
90
src/services/token/token.service.ts
Normal file
@@ -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<void> {
|
||||
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<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TOKEN_KEY);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearToken(): Promise<void> {
|
||||
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<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_ID_KEY);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_SECRET_KEY);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<void> {
|
||||
await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async decodeToken(token?: string): Promise<DecodedToken | null> {
|
||||
const tokenToUse = token ?? (await this.getToken());
|
||||
if (!tokenToUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeJwt(tokenToUse);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise<boolean> {
|
||||
const token = await this.getToken();
|
||||
if (!token) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return checkTokenNeedsRefresh(token, minutesBeforeExpiration);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/utils/jwt.util.ts
Normal file
44
src/utils/jwt.util.ts
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user