1
0
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:
Brandon
2026-02-23 15:50:31 -05:00
parent 77873c3075
commit 4b079a3ec9
56 changed files with 1911 additions and 6769 deletions

745
jslib-removal-plan.md Normal file
View 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.

View File

@@ -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();
}
}

View File

@@ -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 {}

View File

@@ -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);
});
});
});

View File

@@ -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(),
);
});
});
});

View File

@@ -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++) {

View File

@@ -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>;
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -1,4 +0,0 @@
export abstract class StateMigrationService {
needsMigration: () => Promise<boolean>;
migrate: () => Promise<void>;
}

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,
},
});
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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,
);
}
}

View 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;
}

View File

@@ -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>;
}

View File

@@ -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>;
}

View 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>;
}

View File

@@ -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;
}
}

View File

@@ -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 () => {

View File

@@ -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[],
})

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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);
}
}

View File

@@ -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");
});
});

View File

@@ -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:

View File

@@ -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);

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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");
}
}

View File

@@ -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");
});
});
});
});

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) {

View 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
View 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;
}