mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-28 02:03:15 +00:00
Compare commits
18 Commits
state-serv
...
dev-clarit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5afdd238bb | ||
|
|
684cfcbe13 | ||
|
|
4eb74cdeb4 | ||
|
|
b1a3859516 | ||
|
|
4c7afc0e64 | ||
|
|
4b079a3ec9 | ||
|
|
77873c3075 | ||
|
|
9997e988e6 | ||
|
|
abdddacb06 | ||
|
|
d5566c56b1 | ||
|
|
a019555143 | ||
|
|
b3cb369ed8 | ||
|
|
06edf4cf91 | ||
|
|
623382f9e1 | ||
|
|
a0e74948bd | ||
|
|
9f8018e8f8 | ||
|
|
0bff38c459 | ||
|
|
94ff20f69f |
@@ -1,3 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
# Bitwarden Directory Connector
|
||||
|
||||
## Project Overview
|
||||
@@ -20,6 +24,392 @@ Directory Connector is a TypeScript application that synchronizes users and grou
|
||||
- Node
|
||||
- Jest for testing
|
||||
|
||||
### Current Project Status
|
||||
|
||||
**Mission Critical but Deprioritized:** Directory Connector is used to sync customer directory services with their Bitwarden organization. While SCIM is the more modern cloud-hosted solution, not all directory services support SCIM, and SCIM is only available on Enterprise plans. Therefore, DC remains mission-critical infrastructure for many paying customers, but it's deprioritized in the codebase due to infrequent changes.
|
||||
|
||||
**Isolated Repository:** Unlike other Bitwarden client applications that live in a monorepo with shared core libraries, Directory Connector was kept separate when other TypeScript clients moved to the monorepo. It got its own copy of the jslib repo to avoid unnecessary regressions from apparently unrelated code changes in other clients. This severed it from the rest of the codebase, causing:
|
||||
|
||||
- Outdated dependencies that can't be updated (ES modules vs CommonJS conflicts)
|
||||
- File/folder structure that doesn't match modern Bitwarden client patterns
|
||||
- Accumulated technical debt requiring significant investment to pay down
|
||||
- jslib contains unused code from all clients, but cannot be deleted due to monolithic/tightly coupled architecture
|
||||
|
||||
**Critical Issues (Current Status):**
|
||||
|
||||
- ✅ ~~Electron, Node, and Angular are on unmaintained versions~~ **RESOLVED** - All updated (Electron 39, Node 20, Angular 21, TypeScript 5.9)
|
||||
- ❌ `keytar` is archived (Dec 2022) and incompatible with Node v22, **blocking Node upgrades beyond v20** - **PRIMARY BLOCKER**
|
||||
- ❌ No ESM support blocks dependency upgrades: googleapis, lowdb, chalk, inquirer, node-fetch, electron-store
|
||||
- ⚠️ 70 dev dependencies + 31 runtime dependencies = excessive maintenance burden (count increased with Angular 21 tooling)
|
||||
- ❌ StateService is a large pre-StateProvider monolith containing every getter/setter for all clients (PM-31159 In Progress)
|
||||
- ✅ ~~Angular CLI not used~~ **RESOLVED** - Angular CLI 21.1.2 now integrated with angular.json configuration
|
||||
|
||||
**Development Approach:** When working on this codebase, prioritize sustainability and maintainability over adding new features. Consider how changes will affect long-term maintenance burden.
|
||||
|
||||
## Tech Debt Roadmap
|
||||
|
||||
### Progress Summary
|
||||
|
||||
**Completed:**
|
||||
|
||||
- ✅ Phase 0 (Immediate Priority): All major dependencies upgraded (Node 20, Angular 21, TypeScript 5.9, Electron 39)
|
||||
- ✅ Phase 6: Angular CLI integration complete
|
||||
|
||||
**In Progress:**
|
||||
|
||||
- 🔄 Phase 1: StateService rewrite (PM-31159)
|
||||
|
||||
**Blocked/Todo:**
|
||||
|
||||
- ❌ Phase 2: Remove remaining jslib code (blocked by Phase 1)
|
||||
- ❌ Phase 3: Repository restructure (should be done before Phase 5)
|
||||
- ⚠️ Phase 4: Replace Keytar **[CRITICAL BLOCKER]** - blocking Node v22+ upgrades
|
||||
- ❌ Phase 5: ESM Support (blocked by Phase 3, needed for googleapis, lowdb, chalk, inquirer, etc.)
|
||||
|
||||
**Primary Blocker:** Keytar removal (Phase 4) is the most critical task as it blocks Node upgrades beyond v20.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Immediate Priority: Unsupported Dependencies (COMPLETED)
|
||||
|
||||
**Upgrade Path (July 2025 release) - STATUS: COMPLETE**
|
||||
|
||||
All major version upgrades have been completed and exceeded targets:
|
||||
|
||||
1. ✅ Node 18.20.8 → 20.18 → **COMPLETE** (engines: `~20`, .nvmrc: `v20`)
|
||||
2. ✅ Angular 17 → 18.2.x → **EXCEEDED** (now at **21.1.1**)
|
||||
3. ✅ TypeScript 5.4.5 → 5.6.0 → **EXCEEDED** (now at **5.9.3**)
|
||||
4. ✅ Electron 34 → 36 → **EXCEEDED** (now at **39.2.1**)
|
||||
5. ✅ Angular matches clients monorepo version (21.x)
|
||||
|
||||
**Current Versions:**
|
||||
|
||||
- Node: v20 (project target), blocked from v22+ by keytar
|
||||
- TypeScript: 5.9.3
|
||||
- Angular: 21.1.1 (all packages)
|
||||
- Electron: 39.2.1 (well beyond EOL target of 36)
|
||||
- @yao-pkg/pkg: 5.16.1 (community fork replacing archived pkg)
|
||||
|
||||
**Note:** Further Node upgrades to v22+ are **blocked by keytar** (see Phase 4). Electron 36 was EOL October 2028, but we're already on 39.2.1.
|
||||
|
||||
### Phase 1: StateService Rewrite (PM-31159, In Progress)
|
||||
|
||||
**Problem:** StateService is a post-account-switching, pre-StateProvider monolith containing every getter/setter for all clients. This prevents deletion of unused data models and code. Never very stable, and more complex than DC needs (DC doesn't need account switching).
|
||||
|
||||
**Current Status:** 🔄 **Active PR** - [#990](https://github.com/bitwarden/directory-connector/pull/990) (Open, Author: @BTreston)
|
||||
|
||||
- PR created: Feb 2, 2026
|
||||
- Last updated: Feb 5, 2026
|
||||
- Files changed: 17 files (+1,512, -41 lines)
|
||||
- Commits: 4 (scaffold, add tests, fix type issues, fix integration test)
|
||||
|
||||
**Implementation Details:**
|
||||
|
||||
**New Architecture:**
|
||||
|
||||
- Created `StateServiceVNext` interface (`src/abstractions/state-vNext.service.ts`)
|
||||
- New implementation: `StateServiceVNextImplementation` (`src/services/state-service/state-vNext.service.ts`)
|
||||
- New state model with flat key-value structure (`src/models/state.model.ts`)
|
||||
- Comprehensive test suite: `state-vNext.service.spec.ts` (488 lines of tests)
|
||||
|
||||
**Storage Key Structure:**
|
||||
|
||||
```typescript
|
||||
// vNext Storage Keys (Flat key-value structure)
|
||||
StorageKeysVNext = {
|
||||
stateVersion: "stateVersion",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
directory_ldap: "directory_ldap",
|
||||
directory_gsuite: "directory_gsuite",
|
||||
directory_entra: "directory_entra",
|
||||
directory_okta: "directory_okta",
|
||||
directory_onelogin: "directory_onelogin",
|
||||
sync: "sync",
|
||||
syncingDir: "syncingDir",
|
||||
};
|
||||
|
||||
// Secure storage keys for sensitive data
|
||||
SecureStorageKeysVNext = {
|
||||
ldap: "secret_ldap",
|
||||
gsuite: "secret_gsuite",
|
||||
azure: "secret_azure", // Backwards compatible with old name
|
||||
entra: "secret_entra",
|
||||
okta: "secret_okta",
|
||||
oneLogin: "secret_oneLogin",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
```
|
||||
|
||||
**Migration Strategy:**
|
||||
|
||||
- State version bumped to `StateVersion.Five` (`jslib/common/src/enums/stateVersion.ts`)
|
||||
- Enhanced `StateMigrationService` to handle migration from old account-based structure to new flat structure
|
||||
- Migration keys defined for backwards compatibility (`MigrationKeys`, `SecureStorageKeysMigration`)
|
||||
- Temporary keys used during migration (`TempKeys`) to preserve data during transition
|
||||
|
||||
**File Organization:**
|
||||
|
||||
- State-related files moved to `src/services/state-service/` subdirectory:
|
||||
- `state-vNext.service.ts` (new implementation)
|
||||
- `state-vNext.service.spec.ts` (488 lines of tests)
|
||||
- `state.service.ts` (legacy, moved from `src/services/`)
|
||||
- `stateMigration.service.ts` (enhanced for v5 migration)
|
||||
- New abstraction: `src/abstractions/state-vNext.service.ts`
|
||||
- New model: `src/models/state.model.ts` (defines all storage keys)
|
||||
|
||||
**Integration:**
|
||||
|
||||
- Both old `StateService` and new `StateServiceVNext` injected in parallel during migration phase
|
||||
- `DirectoryFactoryService` updated to accept both services
|
||||
- Services module provides both implementations
|
||||
- CLI (`bwdc.ts`) and GUI (`main.ts`) both instantiate new service alongside old one
|
||||
|
||||
**Chosen Approach Benefits:**
|
||||
|
||||
- Clean break with old StateService - high degree of certainty
|
||||
- Simple and focused on DC's needs (no account switching, no rxjs)
|
||||
- Flat key-value structure easier to maintain
|
||||
- Versioning and migration capabilities included
|
||||
- Keeps existing data.json around during transition
|
||||
- All getters/setters in one place (acceptable for small application)
|
||||
|
||||
**Rejected Approaches:**
|
||||
|
||||
- Copy StateProvider from clients: Too complex (supports account switching, rxjs, syncing background/foreground contexts)
|
||||
- Rewrite simplified StateService keeping current data structure: Commits us to previous decisions, keeps monolithic account objects
|
||||
|
||||
**Next Steps:**
|
||||
|
||||
- Complete PR review and merge
|
||||
- Monitor for regressions during initial rollout
|
||||
- After several releases, can remove old StateService and migration code
|
||||
- Begin Phase 2: Remove remaining jslib code that was only needed by old StateService
|
||||
|
||||
### Phase 2: Remove Remaining jslib Code
|
||||
|
||||
After StateService is removed, review and delete old models and remaining services that referenced each other. jslib contains unused code from all clients that DC doesn't need.
|
||||
|
||||
### Phase 3: Restructure Repository (PM-31852, To Do)
|
||||
|
||||
**Current Structure:**
|
||||
|
||||
```
|
||||
src/ # Both Electron and CLI app code
|
||||
src-cli/ # package.json entry point for CLI only, no code
|
||||
jslib/
|
||||
├── common/ # Shared common code
|
||||
├── node/ # Node specific code used in CLI
|
||||
└── electron/ # Electron specific code used in GUI
|
||||
```
|
||||
|
||||
**Target Structure:**
|
||||
|
||||
```
|
||||
src-gui/ # Electron specific code only (combining src (partial) + jslib/electron)
|
||||
src-cli/ # Node and CLI specific code only (combining src (partial) + jslib/node)
|
||||
libs/ # Shared app-independent DC code, e.g. sync services (combining src (partial) + jslib/common)
|
||||
```
|
||||
|
||||
**Why:** Makes subsequent changes (code reorganizing, ESM support) much easier. This should be done early in the modernization process.
|
||||
|
||||
### Phase 4: Replace Keytar (PM-12436, To Do) ⚠️ **CRITICAL BLOCKER**
|
||||
|
||||
**Problem:** `keytar` (OS secure storage for secrets) was archived December 2022 and is incompatible with Node v22, **actively blocking Node upgrades beyond v20**.
|
||||
|
||||
**Current Status:**
|
||||
|
||||
- `keytar`: **7.9.0** (still present in dependencies)
|
||||
- **This is the #1 blocker preventing Node v22+ upgrades**
|
||||
- All "Immediate Priority" dependencies have been upgraded, but further progress requires removing keytar
|
||||
|
||||
**Solution:** Migrate to Bitwarden's Rust implementation in `desktop_native` (same as clients monorepo did)
|
||||
|
||||
1. Implement Rust <-> NAPI integration (like `desktop_native/napi`) from Electron app to Rust code
|
||||
2. Copy, rename, and expose necessary functions
|
||||
3. Point to `desktop_native` crate using git link from DC repo (no need for SDK yet):
|
||||
```rust
|
||||
desktop_core = { git = "https://github.com/bitwarden/clients", rev = "00cf24972d944638bbd1adc00a0ae3eeabb6eb9a" }
|
||||
```
|
||||
|
||||
**Important:** `keytar` uses wrong encoding on Windows (UTF-8 instead of UTF-16). Bitwarden uses UTF-16. Code should contain a migration - ensure old values are migrated correctly during testing.
|
||||
|
||||
**Priority:** This should be prioritized as it's blocking the Node upgrade path and has been archived for over 2 years.
|
||||
|
||||
### Phase 5: Add ESM Support (PM-31850, To Do)
|
||||
|
||||
**Problem:** No ESM module support prevents upgrading key dependencies.
|
||||
|
||||
**Blocked Dependencies (Current Status):**
|
||||
|
||||
- ❌ `googleapis`: **149.0.0** → current (major dependency, disabled in renovate.json5)
|
||||
- ❌ `lowdb`: **1.0.0** → v7
|
||||
- ❌ `@types/lowdb`: **1.0.15** (can be deleted once inquirer is upgraded)
|
||||
- ❌ `@electron/notarize`: **2.5.0** → v3.0.1
|
||||
- ❌ `chalk`: **4.1.2** → v5.3.0
|
||||
- ❌ `inquirer`: **8.2.6** → v12.1.0
|
||||
- ❌ `@types/inquirer`: **8.2.10** (should be deleted when inquirer upgraded)
|
||||
- ❌ `node-fetch`: **2.7.0** → v3.3.2 (should use native Node fetch API when on Node >=21)
|
||||
- ❌ `electron-store`: **8.2.0** → v10.1.0
|
||||
|
||||
**Status:** These dependencies remain blocked as expected. They will stay on old versions until:
|
||||
|
||||
1. Phase 3 (Repository Restructure) is complete
|
||||
2. ESM support is implemented
|
||||
3. Note: These ESM dependencies are primarily used in CLI build, so restructuring first (Phase 3) will limit the impact of ESM migration.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Update tsconfig.json and package.json configurations
|
||||
2. Update import/export syntax to no longer use `require` statements
|
||||
3. Upgrade dependencies to move away from CommonJS (ESM can import CommonJS, but not vice versa)
|
||||
4. Trial and error
|
||||
|
||||
**Reference:** [Pure ESM package guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
|
||||
|
||||
### Phase 6: Add Angular CLI (PM-31849, In Progress / Possibly Complete?)
|
||||
|
||||
**Problem:** Angular CLI provides great DX and makes it easier to manage Angular changes (e.g. auto-migrations). DC didn't use it.
|
||||
|
||||
**Current Status:**
|
||||
|
||||
- ✅ `@angular/cli`: **21.1.2** is now present in **runtime dependencies**
|
||||
- ✅ `@angular/build`: **21.1.2** is present in dev dependencies
|
||||
- ✅ All Angular tooling has been updated to v21.x
|
||||
|
||||
**Status:** ✅ **COMPLETE** - Angular CLI has been successfully integrated:
|
||||
|
||||
- `angular.json` configuration file exists
|
||||
- `.angular/` cache directory present
|
||||
- `@angular/cli` 21.1.2 in runtime dependencies
|
||||
- `@angular/build` 21.1.2 in dev dependencies
|
||||
- All Angular packages updated to v21.x
|
||||
|
||||
This migration provides improved DX and access to Angular's auto-migration tools for future updates.
|
||||
|
||||
### Additional Considerations
|
||||
|
||||
**Reduce Dependency Count:** Current state is 70 dev dependencies + 31 runtime dependencies (101 total). The dev dependency count increased from the original 66 due to Angular 21 upgrade adding additional tooling. After removing old code, review dependency list:
|
||||
|
||||
- Can we remove some after code cleanup?
|
||||
- Could we reintegrate with monorepo to leverage Component Library and shared platform dependencies?
|
||||
- **Risk:** Becomes tightly coupled with monorepo code → regression risk, move slower due to coupling
|
||||
|
||||
**GitHub Workflows:** Need review and modernization:
|
||||
|
||||
- PM-20478: Add check-run workflow for CI on community PRs
|
||||
- PM-18290: Add linting workflow
|
||||
- PM-18289: Update build workflow
|
||||
- `pkg` and `pkg-fetch` for packaging Node runtime in CLI release are archived (fork exists but untrusted; clients vets all changes manually)
|
||||
- Options: Make our own fork, or use Node's single executable binary support (investigate)
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Desktop App (Electron + Angular)
|
||||
|
||||
**Initial Setup:**
|
||||
|
||||
```bash
|
||||
npm install # Install dependencies (runs git submodule init automatically)
|
||||
npm run rebuild # Rebuild native modules for Electron
|
||||
```
|
||||
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
npm run electron # Build and run desktop app with hot reload and debugging
|
||||
npm run electron:ignore # Same as above but ignores certificate errors
|
||||
```
|
||||
|
||||
**Building:**
|
||||
|
||||
```bash
|
||||
npm run build # Build both main and renderer processes
|
||||
npm run build:main # Build Electron main process only
|
||||
npm run build:renderer # Build Angular renderer process only
|
||||
npm run build:renderer:watch # Build renderer with file watching
|
||||
```
|
||||
|
||||
**Distribution:**
|
||||
|
||||
```bash
|
||||
npm run dist:mac # Create macOS distributable
|
||||
npm run dist:win # Create Windows distributable
|
||||
npm run dist:lin # Create Linux distributable
|
||||
```
|
||||
|
||||
### CLI (bwdc)
|
||||
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
npm run build:cli:watch # Build CLI with file watching
|
||||
node ./build-cli/bwdc.js --help # Run the CLI from build output
|
||||
```
|
||||
|
||||
**Production Build:**
|
||||
|
||||
```bash
|
||||
npm run build:cli:prod # Build CLI for production
|
||||
npm run dist:cli # Create platform-specific CLI executables (all platforms)
|
||||
npm run dist:cli:mac # Create macOS CLI executable only
|
||||
npm run dist:cli:win # Create Windows CLI executable only
|
||||
npm run dist:cli:lin # Create Linux CLI executable only
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
**Unit Tests:**
|
||||
|
||||
```bash
|
||||
npm test # Run unit tests (excludes integration tests)
|
||||
npm run test:watch # Run unit tests in watch mode
|
||||
npm run test:watch:all # Run unit tests in watch mode (all files)
|
||||
npm run test:types # Run TypeScript type checking without emitting files
|
||||
```
|
||||
|
||||
**Integration Tests:**
|
||||
|
||||
```bash
|
||||
npm run test:integration:setup # Set up Docker containers for LDAP testing
|
||||
npm run test:integration # Run integration tests
|
||||
npm run test:integration:watch # Run integration tests in watch mode
|
||||
```
|
||||
|
||||
Integration tests require Docker and test against live directory services. The setup command creates OpenLDAP containers using docker-compose.yml.
|
||||
|
||||
### Linting & Formatting
|
||||
|
||||
```bash
|
||||
npm run lint # Run ESLint and Prettier checks
|
||||
npm run lint:fix # Auto-fix ESLint issues
|
||||
npm run prettier # Format all files with Prettier
|
||||
```
|
||||
|
||||
### Submodule Management
|
||||
|
||||
The `jslib` folder is a git submodule containing shared Bitwarden libraries:
|
||||
|
||||
```bash
|
||||
npm run sub:update # Update submodule to latest remote version
|
||||
npm run sub:pull # Pull latest changes in submodule
|
||||
npm run sub:commit # Pull and commit submodule update
|
||||
```
|
||||
|
||||
### Utility Commands
|
||||
|
||||
```bash
|
||||
npm run reset # Remove keytar modules and reinstall (use when switching between CLI/desktop)
|
||||
npm run clean:dist # Clean desktop distribution files
|
||||
npm run clean:dist:cli # Clean CLI distribution files
|
||||
```
|
||||
|
||||
**Important:** When switching between developing the desktop app and CLI, run `npm run reset` to avoid native module conflicts.
|
||||
|
||||
## Code Architecture & Structure
|
||||
|
||||
### Directory Organization
|
||||
@@ -45,6 +435,32 @@ jslib/ # Legacy folder structure (mix of deprecated/unused and c
|
||||
3. **Directory Service Pattern**: Each directory provider implements `IDirectoryService` interface
|
||||
4. **Separation of Concerns**: GUI (Angular app) and CLI (commands) share the same service layer
|
||||
|
||||
### Core Synchronization Flow
|
||||
|
||||
The sync process follows this pattern:
|
||||
|
||||
1. **DirectoryFactoryService** (`src/services/directory-factory.service.ts`) - Creates the appropriate directory service based on DirectoryType configuration
|
||||
2. **IDirectoryService** implementation (`src/services/directory-services/*.service.ts`) - Each provider (LDAP, Entra ID, Google, Okta, OneLogin) implements:
|
||||
- `getEntries(force, test)` - Returns `[GroupEntry[], UserEntry[]]`
|
||||
- Provider-specific authentication and API calls
|
||||
3. **SyncService** (`src/services/sync.service.ts`) - Orchestrates the sync:
|
||||
- Calls directory service to get entries
|
||||
- Filters and deduplicates users/groups
|
||||
- Uses BatchRequestBuilder or SingleRequestBuilder to format API requests
|
||||
- Generates hash to detect changes and avoid redundant syncs
|
||||
- Sends data to Bitwarden API via ApiService
|
||||
4. **Request Builders** (`src/services/*-request-builder.ts`) - Transform directory entries into Bitwarden API format
|
||||
|
||||
### Shared Library (jslib)
|
||||
|
||||
The `jslib` folder is a git submodule containing shared Bitwarden code:
|
||||
|
||||
- Common services (API, Crypto, Storage, Auth)
|
||||
- Platform utilities
|
||||
- Shared models and abstractions
|
||||
|
||||
**Important:** This is legacy structure - do not add new code to jslib. New code should go in `src/`.
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Code Organization
|
||||
|
||||
239
.claude/plan.md
Normal file
239
.claude/plan.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# Phase 2 PR #1: Flatten Account Model - IMPLEMENTATION COMPLETE
|
||||
|
||||
## Status: ✅ COMPLETED
|
||||
|
||||
**Implementation Date:** February 13, 2026
|
||||
**All tests passing:** 120/120 ✅
|
||||
**TypeScript compilation:** Success ✅
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Successfully implemented Phase 2 PR #1: Flatten Account Model. The Account model has been simplified from 177 lines (51 + 126 inherited) to 51 lines, removing the BaseAccount inheritance and flattening nested structures into direct properties.
|
||||
|
||||
## Changes Implemented
|
||||
|
||||
### Files Modified (7 files)
|
||||
|
||||
1. **`jslib/common/src/enums/stateVersion.ts`**
|
||||
- Added `StateVersion.Five` for the flattened Account structure
|
||||
- Updated `StateVersion.Latest = Five`
|
||||
|
||||
2. **`src/models/account.ts`**
|
||||
- Removed `extends BaseAccount` inheritance
|
||||
- Removed `ClientKeys` class (redundant)
|
||||
- Flattened 6 authentication fields to top level:
|
||||
- `userId`, `entityId`, `apiKeyClientId`
|
||||
- `accessToken`, `refreshToken`, `apiKeyClientSecret`
|
||||
- Kept `DirectoryConfigurations` and `DirectorySettings` unchanged
|
||||
- Added compatibility fields with FIXME comment for jslib infrastructure:
|
||||
- `data?`, `keys?`, `profile?`, `settings?`, `tokens?` (optional, unused)
|
||||
- Simplified constructor without Object.assign
|
||||
|
||||
3. **`src/services/stateMigration.service.ts`**
|
||||
- Added `migrateStateFrom3To4()` placeholder migration
|
||||
- Added `migrateStateFrom4To5()` to flatten nested → flat Account structure
|
||||
- Updated `migrate()` method with new case statements for v3→v4 and v4→v5
|
||||
- Updated `migrateStateFrom1To2()` to use flattened structure (removed `account.profile`, `account.clientKeys`)
|
||||
|
||||
4. **`src/services/auth.service.ts`**
|
||||
- Removed imports: `AccountKeys`, `AccountProfile`, `AccountTokens`
|
||||
- Simplified account creation from 26 lines to 10 lines (62% reduction)
|
||||
- Direct property assignment instead of nested objects with spread operators
|
||||
|
||||
5. **`src/services/state.service.ts`**
|
||||
- Changed `account.profile.userId` → `account.userId`
|
||||
- Removed `account.settings` from `scaffoldNewAccountDiskStorage`
|
||||
- Added `settings` back to `resetAccount` for base class compatibility (unused but required)
|
||||
|
||||
6. **`src/services/authService.spec.ts`**
|
||||
- Removed imports: `AccountKeys`, `AccountProfile`, `AccountTokens`
|
||||
- Updated test expectations to match new flat Account structure
|
||||
|
||||
### Files Created (1 file)
|
||||
|
||||
7. **`src/services/stateMigration.service.spec.ts`**
|
||||
- Comprehensive migration test suite (5 tests, 210 lines)
|
||||
- Tests flattening nested account structure
|
||||
- Tests handling missing nested objects gracefully
|
||||
- Tests empty account list
|
||||
- Tests preservation of directory configurations and settings
|
||||
- Tests state version update
|
||||
|
||||
## Code Reduction Achieved
|
||||
|
||||
- **Account model:** 177 lines (51 + 126 inherited) → 51 lines (71% reduction)
|
||||
- **AuthService account creation:** 26 lines → 10 lines (62% reduction)
|
||||
- **Import statements removed:** 5 jslib imports across multiple files
|
||||
|
||||
## Migration Logic
|
||||
|
||||
### State Version v4 → v5 Migration
|
||||
|
||||
The `migrateStateFrom4To5()` method handles conversion from nested to flat structure:
|
||||
|
||||
```typescript
|
||||
// OLD (nested structure):
|
||||
{
|
||||
profile: {
|
||||
userId: "CLIENT_ID",
|
||||
entityId: "CLIENT_ID",
|
||||
apiKeyClientId: "organization.CLIENT_ID"
|
||||
},
|
||||
tokens: {
|
||||
accessToken: "token",
|
||||
refreshToken: "refresh"
|
||||
},
|
||||
keys: {
|
||||
apiKeyClientSecret: "secret"
|
||||
}
|
||||
}
|
||||
|
||||
// NEW (flat structure):
|
||||
{
|
||||
userId: "CLIENT_ID",
|
||||
entityId: "CLIENT_ID",
|
||||
apiKeyClientId: "organization.CLIENT_ID",
|
||||
accessToken: "token",
|
||||
refreshToken: "refresh",
|
||||
apiKeyClientSecret: "secret"
|
||||
}
|
||||
```
|
||||
|
||||
**Migration Safety:**
|
||||
|
||||
- Null-safe property access with `??` operator
|
||||
- Preserves all directory configurations and settings
|
||||
- Falls back to userId if profile.userId doesn't exist
|
||||
- Handles empty account lists gracefully
|
||||
|
||||
## Test Results
|
||||
|
||||
### Unit Tests: ✅ PASS
|
||||
|
||||
```
|
||||
Test Suites: 14 passed, 14 total
|
||||
Tests: 120 passed, 120 total
|
||||
```
|
||||
|
||||
New tests added:
|
||||
|
||||
- `should flatten nested account structure` ✅
|
||||
- `should handle missing nested objects gracefully` ✅
|
||||
- `should handle empty account list` ✅
|
||||
- `should preserve directory configurations and settings` ✅
|
||||
- `should update state version after successful migration` ✅
|
||||
|
||||
### TypeScript Compilation: ✅ PASS
|
||||
|
||||
```
|
||||
npm run test:types
|
||||
```
|
||||
|
||||
All type checks pass with zero errors.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Compatibility Fields
|
||||
|
||||
Added optional compatibility fields to Account model to satisfy jslib infrastructure type constraints:
|
||||
|
||||
```typescript
|
||||
// FIXME: Remove these compatibility fields after StateServiceVNext migration (PR #990) is merged
|
||||
// These fields are unused but required for type compatibility with jslib's StateService infrastructure
|
||||
data?: any;
|
||||
keys?: any;
|
||||
profile?: any;
|
||||
settings?: any;
|
||||
tokens?: any;
|
||||
```
|
||||
|
||||
These will be removed after PR #990 (StateServiceVNext) merges and old StateService is deleted.
|
||||
|
||||
### Key Architectural Decision
|
||||
|
||||
Chose to add compatibility fields rather than refactor entire jslib infrastructure because:
|
||||
|
||||
1. PR #990 (StateServiceVNext) will eventually replace this infrastructure
|
||||
2. Minimizes changes needed in this PR
|
||||
3. Avoids conflicts with PR #990
|
||||
4. Can be cleaned up later
|
||||
|
||||
## What This Enables
|
||||
|
||||
### Immediate Benefits
|
||||
|
||||
- ✅ Simplified Account model (71% code reduction)
|
||||
- ✅ Clearer authentication field structure
|
||||
- ✅ Easier debugging (no nested property access)
|
||||
- ✅ Self-documenting code (obvious what DC needs)
|
||||
|
||||
### Enables Future Work
|
||||
|
||||
- **Phase 2 PR #2:** Remove StateFactory infrastructure
|
||||
- **Phase 2 PR #3:** Delete ~90 unused jslib files including:
|
||||
- EncString (only used by old nested Account)
|
||||
- SymmetricCryptoKey (only used by old nested Account)
|
||||
- OrganizationData (completely unused)
|
||||
- ProviderData (completely unused)
|
||||
- AccountKeys, AccountProfile, AccountTokens, AccountData, AccountSettings
|
||||
|
||||
## Merge Strategy
|
||||
|
||||
**Conflict Management:**
|
||||
|
||||
- This PR targets current codebase (with old StateService)
|
||||
- Will conflict with PR #990 (StateServiceVNext) when it merges
|
||||
- Plan: Rebase this PR after #990 merges
|
||||
- Expected conflicts: StateService files, Account model structure
|
||||
- Resolution: Keep StateServiceVNext changes, apply Account flattening to new structure
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review & Test:** Thorough code review and manual testing
|
||||
2. **Create PR:** Open PR with comprehensive description and test results
|
||||
3. **Manual Testing Scenarios:**
|
||||
- Fresh installation → authentication flow
|
||||
- Existing installation → migration runs successfully
|
||||
- All directory types → configuration persists correctly
|
||||
- CLI authentication → flat structure works
|
||||
4. **After Merge:**
|
||||
- Begin Phase 2 PR #2: Remove StateFactory Infrastructure
|
||||
- Monitor for any migration issues in production
|
||||
|
||||
## Related Work
|
||||
|
||||
- **Depends On:** None (can merge independently)
|
||||
- **Blocks:** Phase 2 PR #2 (Remove StateFactory), Phase 2 PR #3 (Delete Unused jslib Files)
|
||||
- **Conflicts With:** PR #990 (StateServiceVNext) - plan to rebase after #990 merges
|
||||
- **Part Of:** Phase 2 tech debt cleanup (see CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
## Original Implementation Plan
|
||||
|
||||
[The original detailed step-by-step plan from the conversation has been preserved below for reference]
|
||||
|
||||
### Context
|
||||
|
||||
Directory Connector's Account model currently extends jslib's BaseAccount, inheriting 126 lines of complex nested structures designed for multi-account password manager features that DC doesn't use. This inheritance creates unnecessary coupling and blocks cleanup of unused jslib dependencies.
|
||||
|
||||
**Current State:**
|
||||
|
||||
- Account extends BaseAccount with nested objects: `profile.userId`, `tokens.accessToken`, `keys.apiKeyClientSecret`
|
||||
- Only 6 fields from BaseAccount are actually used by DC
|
||||
- 120+ lines of inherited code (AccountData, AccountKeys, AccountProfile, AccountSettings, AccountTokens) are unused
|
||||
- Creates dependencies on EncString, SymmetricCryptoKey, OrganizationData, ProviderData that DC never uses
|
||||
|
||||
**Problem:**
|
||||
|
||||
- Unnecessary complexity for a single-account application
|
||||
- Blocks deletion of unused jslib models (Phase 2 goal)
|
||||
- Verbose account creation code (26 lines to set 6 fields)
|
||||
- Difficult to understand what DC actually needs
|
||||
|
||||
**Goal:**
|
||||
Flatten Account model to contain only the 8 fields DC uses, removing BaseAccount inheritance. This enables Phase 2 PR #2 and PR #3 to delete ~90 unused jslib files.
|
||||
|
||||
[Rest of original plan preserved in conversation transcript]
|
||||
130
.claude/skills/commonjs-to-esm/skill.md
Normal file
130
.claude/skills/commonjs-to-esm/skill.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
userInvocable: true
|
||||
---
|
||||
|
||||
# CommonJS to ESM Conversion
|
||||
|
||||
Convert a file (or files) from CommonJS module syntax to ECMAScript Modules (ESM).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/commonjs-to-esm <file-path> [additional-file-paths...]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `file-path` - Path to the file(s) to convert from CommonJS to ESM
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
/commonjs-to-esm src/services/auth.service.ts
|
||||
/commonjs-to-esm src/utils/helper.ts src/utils/parser.ts
|
||||
```
|
||||
|
||||
## Process
|
||||
|
||||
This skill performs a comprehensive analysis and planning process:
|
||||
|
||||
### 1. Analyze Target File(s)
|
||||
|
||||
For each file to convert:
|
||||
|
||||
- Read the file contents
|
||||
- Identify its purpose and functionality
|
||||
- Catalog all CommonJS patterns used:
|
||||
- `require()` statements
|
||||
- `module.exports` assignments
|
||||
- `exports.x = ...` assignments
|
||||
- Dynamic requires
|
||||
- `__dirname` and `__filename` usage
|
||||
|
||||
### 2. Find Dependents
|
||||
|
||||
- Search for all files that import/require the target file(s)
|
||||
- Identify the import patterns used by dependents
|
||||
- Map the dependency tree to understand impact scope
|
||||
|
||||
### 3. Analyze Dependencies
|
||||
|
||||
- List all modules the target file(s) depend on
|
||||
- Determine if dependencies support ESM
|
||||
- Identify potential blocking dependencies (CommonJS-only packages)
|
||||
- Check for dynamic imports that may need special handling
|
||||
|
||||
### 4. Identify Conversion Challenges
|
||||
|
||||
Common issues to flag:
|
||||
|
||||
- `__dirname` and `__filename` (need `import.meta.url` conversion)
|
||||
- Dynamic `require()` calls (need `import()` conversion)
|
||||
- Conditional requires (need refactoring)
|
||||
- JSON imports (need `assert { type: 'json' }`)
|
||||
- CommonJS-only dependencies (may block conversion)
|
||||
- Circular dependencies (may need restructuring)
|
||||
|
||||
### 5. Generate Conversion Plan
|
||||
|
||||
Create a step-by-step plan that includes:
|
||||
|
||||
**Target File Changes:**
|
||||
|
||||
- Convert `require()` to `import` statements
|
||||
- Convert `module.exports` to `export` statements
|
||||
- Update `__dirname`/`__filename` to use `import.meta.url`
|
||||
- Handle dynamic imports appropriately
|
||||
- Update file extensions if needed (e.g., `.js` to `.mjs`)
|
||||
|
||||
**Dependent File Changes:**
|
||||
|
||||
- Update all import statements in dependent files
|
||||
- Ensure consistent naming (default vs named exports)
|
||||
- Update path references if extensions change
|
||||
|
||||
**Configuration Changes:**
|
||||
|
||||
- `package.json`: Add `"type": "module"` or use `.mjs` extension
|
||||
- `tsconfig.json`: Update `module` and `moduleResolution` settings
|
||||
- Build tools: Update bundler/compiler configurations
|
||||
|
||||
**Testing Strategy:**
|
||||
|
||||
- Run unit tests after conversion
|
||||
- Verify no runtime errors from import changes
|
||||
- Check that all exports are accessible
|
||||
- Test dynamic import scenarios
|
||||
|
||||
### 6. Risk Assessment
|
||||
|
||||
Evaluate:
|
||||
|
||||
- Number of files affected
|
||||
- Complexity of CommonJS patterns used
|
||||
- Presence of blocking dependencies
|
||||
- Potential for breaking changes
|
||||
|
||||
### 7. Present Plan
|
||||
|
||||
Output a structured plan with:
|
||||
|
||||
- Summary of changes needed
|
||||
- Ordered steps for execution
|
||||
- List of files to modify
|
||||
- Configuration changes required
|
||||
- Testing checkpoints
|
||||
- Risk factors and mitigation strategies
|
||||
- Estimated scope (small/medium/large change)
|
||||
|
||||
## Notes
|
||||
|
||||
- ESM is **not** compatible with CommonJS in all cases - ESM can import CommonJS, but CommonJS **cannot** require ESM
|
||||
- This means conversions should generally proceed from leaf dependencies upward
|
||||
- Some packages remain CommonJS-only and may block full conversion
|
||||
- The skill generates a plan but does NOT automatically execute the conversion - review and approve first
|
||||
|
||||
## References
|
||||
|
||||
- [Pure ESM package guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
|
||||
- [Node.js ESM documentation](https://nodejs.org/api/esm.html)
|
||||
- [TypeScript ESM support](https://www.typescriptlang.org/docs/handbook/esm-node.html)
|
||||
@@ -10,7 +10,7 @@
|
||||
"output": "dist",
|
||||
"app": "build"
|
||||
},
|
||||
"afterSign": "scripts/notarize.js",
|
||||
"afterSign": "scripts/notarize.mjs",
|
||||
"mac": {
|
||||
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
|
||||
"category": "public.app-category.productivity",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"win": {
|
||||
"target": ["portable", "nsis"],
|
||||
"sign": "scripts/sign.js"
|
||||
"sign": "scripts/sign.mjs"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
import { pathsToModuleNameMapper } from "ts-jest";
|
||||
import tsconfig from "./tsconfig.json" with { type: "json" };
|
||||
|
||||
const tsPreset = require("ts-jest/jest-preset");
|
||||
const angularPreset = require("jest-preset-angular/jest-preset");
|
||||
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
|
||||
import angularPresetsModule from "jest-preset-angular/presets/index.js";
|
||||
|
||||
const { defaultTransformerOptions } = angularPresetsModule;
|
||||
|
||||
const { compilerOptions } = tsconfig;
|
||||
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
// ...tsPreset,
|
||||
// ...angularPreset,
|
||||
export default {
|
||||
preset: "jest-preset-angular",
|
||||
|
||||
reporters: ["default", "jest-junit"],
|
||||
745
jslib-removal-plan.md
Normal file
745
jslib-removal-plan.md
Normal file
@@ -0,0 +1,745 @@
|
||||
# Plan: Remove StateService and jslib Dependencies
|
||||
|
||||
## Context
|
||||
|
||||
Directory Connector currently depends on StateService from jslib, which is a massive pre-StateProvider monolith containing 200+ getter/setter methods for all Bitwarden clients. This creates significant maintenance burden and blocks deletion of unused jslib code.
|
||||
|
||||
**Current State (Phase 1 Complete):**
|
||||
|
||||
- ✅ StateServiceVNext has been implemented with a flat key-value structure
|
||||
- ✅ Migration service handles transition from old account-based structure to new flat structure
|
||||
- ⚠️ Both old and new StateService implementations coexist during migration
|
||||
- ❌ Three jslib services still depend on old StateService: TokenService, CryptoService, EnvironmentService
|
||||
- ❌ Two Electron components depend on old StateService: WindowMain, TrayMain
|
||||
|
||||
**Problem:**
|
||||
The old StateService cannot be removed until all dependencies are eliminated. Analysis reveals:
|
||||
|
||||
- **TokenService**: Used for API authentication (9/32 methods actually used)
|
||||
- **CryptoService**: Completely unused by DC (0/61 methods used) - carried over from monolith
|
||||
- **EnvironmentService**: Used for custom server URLs (4/11 methods used)
|
||||
- **WindowMain/TrayMain**: Used for Electron window/tray state persistence (6 methods total)
|
||||
|
||||
**Goal:**
|
||||
Replace jslib services with simplified DC-specific implementations that use StateServiceVNext, enabling complete removal of old StateService and unlocking Phase 2 (jslib code cleanup).
|
||||
|
||||
**User Decisions:**
|
||||
|
||||
1. ✅ Create simplified DC-specific versions of Token/Environment services (clean break from jslib)
|
||||
2. ✅ Keep WindowMain/TrayMain as-is (minimize scope, focus on StateService removal)
|
||||
3. ✅ Automatic migration on first launch (transparent to users)
|
||||
|
||||
## Critical Files
|
||||
|
||||
### Files to Create (New Implementations)
|
||||
|
||||
- `src/services/token/token.service.ts` - DC-specific token service
|
||||
- `src/abstractions/token.service.ts` - Token service interface
|
||||
- `src/services/environment/environment.service.ts` - DC-specific environment service
|
||||
- `src/abstractions/environment.service.ts` - Environment service interface
|
||||
- `src/utils/jwt.util.ts` - JWT decoding utility (no dependencies)
|
||||
|
||||
### Files to Modify (Update Dependencies)
|
||||
|
||||
- `src/services/api.service.ts` - Switch from jslib TokenService to DC TokenService
|
||||
- `src/services/auth.service.ts` - Update EnvironmentService import
|
||||
- `src/services/sync.service.ts` - Update EnvironmentService import
|
||||
- `src/commands/config.command.ts` - Update EnvironmentService import
|
||||
- `src/bwdc.ts` - Remove old StateService, instantiate new services
|
||||
- `src/main.ts` - Remove old StateService, instantiate new services
|
||||
- `src/app/services/services.module.ts` - Remove old StateService, provide new services
|
||||
- `src/app/app.component.ts` - Update TokenService import
|
||||
- `jslib/electron/src/window.main.ts` - Adapt to use StateServiceVNext
|
||||
- `jslib/electron/src/tray.main.ts` - Adapt to use StateServiceVNext
|
||||
|
||||
### Files to Delete (After Migration)
|
||||
|
||||
- `jslib/common/src/services/token.service.ts` - jslib TokenService
|
||||
- `jslib/common/src/abstractions/token.service.ts` - jslib TokenService interface
|
||||
- `jslib/common/src/services/crypto.service.ts` - Unused CryptoService
|
||||
- `jslib/common/src/abstractions/crypto.service.ts` - Unused CryptoService interface
|
||||
- `jslib/common/src/services/environment.service.ts` - jslib EnvironmentService
|
||||
- `jslib/common/src/abstractions/environment.service.ts` - jslib EnvironmentService interface
|
||||
- `src/services/state-service/state.service.ts` - Old DC StateService
|
||||
- `src/abstractions/state.service.ts` - Old DC StateService interface
|
||||
- `jslib/common/src/services/state.service.ts` - Old jslib StateService
|
||||
- `jslib/common/src/abstractions/state.service.ts` - Old jslib StateService interface
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Create JWT Utility (No Dependencies)
|
||||
|
||||
Create `src/utils/jwt.util.ts` with standalone JWT decoding function:
|
||||
|
||||
```typescript
|
||||
export interface DecodedToken {
|
||||
exp: number;
|
||||
iat: number;
|
||||
nbf: number;
|
||||
sub: string; // user ID
|
||||
client_id?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function decodeJwt(token: string): DecodedToken {
|
||||
// Validate JWT structure (3 parts: header.payload.signature)
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid JWT format");
|
||||
}
|
||||
|
||||
// Decode payload (base64url to JSON)
|
||||
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
return JSON.parse(atob(payload));
|
||||
}
|
||||
|
||||
export function getTokenExpirationDate(token: string): Date | null {
|
||||
const decoded = decodeJwt(token);
|
||||
if (!decoded.exp) return null;
|
||||
return new Date(decoded.exp * 1000);
|
||||
}
|
||||
|
||||
export function tokenSecondsRemaining(token: string, offsetSeconds = 0): number {
|
||||
const expDate = getTokenExpirationDate(token);
|
||||
if (!expDate) return 0;
|
||||
|
||||
const msRemaining = expDate.getTime() - Date.now() - offsetSeconds * 1000;
|
||||
return Math.floor(msRemaining / 1000);
|
||||
}
|
||||
|
||||
export function tokenNeedsRefresh(token: string, minutesBeforeExpiration = 5): boolean {
|
||||
const secondsRemaining = tokenSecondsRemaining(token);
|
||||
return secondsRemaining < minutesBeforeExpiration * 60;
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Standalone utility avoids service dependencies, can be tested independently, reusable.
|
||||
|
||||
### Step 2: Create DC TokenService
|
||||
|
||||
Create `src/abstractions/token.service.ts`:
|
||||
|
||||
```typescript
|
||||
export interface TokenService {
|
||||
// Token storage
|
||||
setTokens(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret?: [string, string],
|
||||
): Promise<void>;
|
||||
getToken(): Promise<string | null>;
|
||||
getRefreshToken(): Promise<string | null>;
|
||||
clearToken(): Promise<void>;
|
||||
|
||||
// API key authentication
|
||||
getClientId(): Promise<string | null>;
|
||||
getClientSecret(): Promise<string | null>;
|
||||
|
||||
// Two-factor token (rarely used)
|
||||
getTwoFactorToken(): Promise<string | null>;
|
||||
clearTwoFactorToken(): Promise<void>;
|
||||
|
||||
// Token validation (delegates to jwt.util)
|
||||
decodeToken(token?: string): Promise<DecodedToken | null>;
|
||||
tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
Create `src/services/token/token.service.ts`:
|
||||
|
||||
```typescript
|
||||
import { StateServiceVNext } from "@/abstractions/state-vNext.service";
|
||||
import { SecureStorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { TokenService as ITokenService } from "@/abstractions/token.service";
|
||||
import {
|
||||
decodeJwt,
|
||||
tokenNeedsRefresh as checkTokenNeedsRefresh,
|
||||
DecodedToken,
|
||||
} from "@/utils/jwt.util";
|
||||
|
||||
export class TokenService implements ITokenService {
|
||||
// Storage keys
|
||||
private TOKEN_KEY = "accessToken";
|
||||
private REFRESH_TOKEN_KEY = "refreshToken";
|
||||
private CLIENT_ID_KEY = "apiKeyClientId";
|
||||
private CLIENT_SECRET_KEY = "apiKeyClientSecret";
|
||||
private TWO_FACTOR_TOKEN_KEY = "twoFactorToken";
|
||||
|
||||
constructor(
|
||||
private stateService: StateServiceVNext,
|
||||
private secureStorageService: SecureStorageService,
|
||||
) {}
|
||||
|
||||
async setTokens(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret?: [string, string],
|
||||
): Promise<void> {
|
||||
await this.secureStorageService.save(this.TOKEN_KEY, accessToken);
|
||||
await this.secureStorageService.save(this.REFRESH_TOKEN_KEY, refreshToken);
|
||||
|
||||
if (clientIdClientSecret) {
|
||||
await this.secureStorageService.save(this.CLIENT_ID_KEY, clientIdClientSecret[0]);
|
||||
await this.secureStorageService.save(this.CLIENT_SECRET_KEY, clientIdClientSecret[1]);
|
||||
}
|
||||
}
|
||||
|
||||
async getToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TOKEN_KEY);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearToken(): Promise<void> {
|
||||
await this.secureStorageService.remove(this.TOKEN_KEY);
|
||||
await this.secureStorageService.remove(this.REFRESH_TOKEN_KEY);
|
||||
await this.secureStorageService.remove(this.CLIENT_ID_KEY);
|
||||
await this.secureStorageService.remove(this.CLIENT_SECRET_KEY);
|
||||
}
|
||||
|
||||
async getClientId(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_ID_KEY);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_SECRET_KEY);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<void> {
|
||||
await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async decodeToken(token?: string): Promise<DecodedToken | null> {
|
||||
const tokenToUse = token ?? (await this.getToken());
|
||||
if (!tokenToUse) return null;
|
||||
|
||||
try {
|
||||
return decodeJwt(tokenToUse);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise<boolean> {
|
||||
const token = await this.getToken();
|
||||
if (!token) return true;
|
||||
|
||||
try {
|
||||
return checkTokenNeedsRefresh(token, minutesBeforeExpiration);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `src/services/token/token.service.spec.ts` with comprehensive tests covering:
|
||||
|
||||
- Token storage/retrieval
|
||||
- Token clearing
|
||||
- JWT decoding
|
||||
- Token expiration logic
|
||||
- Error handling for malformed tokens
|
||||
|
||||
### Step 3: Create DC EnvironmentService
|
||||
|
||||
Create `src/abstractions/environment.service.ts`:
|
||||
|
||||
```typescript
|
||||
export interface EnvironmentUrls {
|
||||
base?: string;
|
||||
api?: string;
|
||||
identity?: string;
|
||||
webVault?: string;
|
||||
icons?: string;
|
||||
notifications?: string;
|
||||
events?: string;
|
||||
keyConnector?: string;
|
||||
}
|
||||
|
||||
export interface EnvironmentService {
|
||||
setUrls(urls: EnvironmentUrls): Promise<void>;
|
||||
setUrlsFromStorage(): Promise<void>;
|
||||
|
||||
hasBaseUrl(): boolean;
|
||||
getApiUrl(): string;
|
||||
getIdentityUrl(): string;
|
||||
getWebVaultUrl(): string;
|
||||
getIconsUrl(): string;
|
||||
getNotificationsUrl(): string;
|
||||
getEventsUrl(): string;
|
||||
getKeyConnectorUrl(): string;
|
||||
}
|
||||
```
|
||||
|
||||
Create `src/services/environment/environment.service.ts`:
|
||||
|
||||
```typescript
|
||||
import { StateServiceVNext } from "@/abstractions/state-vNext.service";
|
||||
import {
|
||||
EnvironmentService as IEnvironmentService,
|
||||
EnvironmentUrls,
|
||||
} from "@/abstractions/environment.service";
|
||||
|
||||
export class EnvironmentService implements IEnvironmentService {
|
||||
private readonly DEFAULT_URLS = {
|
||||
api: "https://api.bitwarden.com",
|
||||
identity: "https://identity.bitwarden.com",
|
||||
webVault: "https://vault.bitwarden.com",
|
||||
icons: "https://icons.bitwarden.net",
|
||||
notifications: "https://notifications.bitwarden.com",
|
||||
events: "https://events.bitwarden.com",
|
||||
};
|
||||
|
||||
private urls: EnvironmentUrls = {};
|
||||
|
||||
constructor(private stateService: StateServiceVNext) {}
|
||||
|
||||
async setUrls(urls: EnvironmentUrls): Promise<void> {
|
||||
// Normalize URLs: trim whitespace, remove trailing slashes, add https:// if missing
|
||||
const normalized: EnvironmentUrls = {};
|
||||
|
||||
for (const [key, value] of Object.entries(urls)) {
|
||||
if (!value) continue;
|
||||
|
||||
let url = value.trim();
|
||||
url = url.replace(/\/+$/, ""); // Remove trailing slashes
|
||||
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
normalized[key] = url;
|
||||
}
|
||||
|
||||
this.urls = normalized;
|
||||
await this.stateService.setEnvironmentUrls(normalized);
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const stored = await this.stateService.getEnvironmentUrls();
|
||||
this.urls = stored ?? {};
|
||||
}
|
||||
|
||||
hasBaseUrl(): boolean {
|
||||
return !!this.urls.base;
|
||||
}
|
||||
|
||||
getApiUrl(): string {
|
||||
return this.urls.api ?? this.urls.base + "/api" ?? this.DEFAULT_URLS.api;
|
||||
}
|
||||
|
||||
getIdentityUrl(): string {
|
||||
return this.urls.identity ?? this.urls.base + "/identity" ?? this.DEFAULT_URLS.identity;
|
||||
}
|
||||
|
||||
getWebVaultUrl(): string {
|
||||
return this.urls.webVault ?? this.urls.base ?? this.DEFAULT_URLS.webVault;
|
||||
}
|
||||
|
||||
getIconsUrl(): string {
|
||||
return this.urls.icons ?? this.urls.base + "/icons" ?? this.DEFAULT_URLS.icons;
|
||||
}
|
||||
|
||||
getNotificationsUrl(): string {
|
||||
return (
|
||||
this.urls.notifications ??
|
||||
this.urls.base + "/notifications" ??
|
||||
this.DEFAULT_URLS.notifications
|
||||
);
|
||||
}
|
||||
|
||||
getEventsUrl(): string {
|
||||
return this.urls.events ?? this.urls.base + "/events" ?? this.DEFAULT_URLS.events;
|
||||
}
|
||||
|
||||
getKeyConnectorUrl(): string {
|
||||
return this.urls.keyConnector ?? "";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `src/services/environment/environment.service.spec.ts` with tests covering:
|
||||
|
||||
- URL normalization (trailing slashes, https prefix)
|
||||
- Storage persistence
|
||||
- Default URL fallbacks
|
||||
- Custom URL override
|
||||
- Base URL derivation
|
||||
|
||||
### Step 4: Add Environment URL Storage to StateServiceVNext
|
||||
|
||||
Update `src/models/state.model.ts` to add environment URL storage key:
|
||||
|
||||
```typescript
|
||||
export const StorageKeysVNext = {
|
||||
// ... existing keys ...
|
||||
environmentUrls: "environmentUrls",
|
||||
};
|
||||
```
|
||||
|
||||
Update `src/abstractions/state-vNext.service.ts` to add methods:
|
||||
|
||||
```typescript
|
||||
export interface StateServiceVNext {
|
||||
// ... existing methods ...
|
||||
getEnvironmentUrls(): Promise<EnvironmentUrls | null>;
|
||||
setEnvironmentUrls(urls: EnvironmentUrls): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
Update `src/services/state-service/state-vNext.service.ts` implementation to add storage methods.
|
||||
|
||||
### Step 5: Update StateMigrationService for Token/Environment Data
|
||||
|
||||
Update `src/services/state-service/stateMigration.service.ts` to migrate:
|
||||
|
||||
**Token data (from secure storage):**
|
||||
|
||||
- `accessToken` → `accessToken` (same key, no change needed)
|
||||
- `refreshToken` → `refreshToken` (same key, no change needed)
|
||||
- `apiKeyClientId` → `apiKeyClientId` (same key, no change needed)
|
||||
- `apiKeyClientSecret` → `apiKeyClientSecret` (same key, no change needed)
|
||||
|
||||
**Environment URLs (from account state):**
|
||||
|
||||
- `environmentUrls` from account → `environmentUrls` in flat structure
|
||||
|
||||
Add migration test cases to `src/services/state-service/stateMigration.service.spec.ts`.
|
||||
|
||||
### Step 6: Remove CryptoService Dependencies
|
||||
|
||||
Since CryptoService is completely unused by DC:
|
||||
|
||||
1. Search for all imports of `CryptoService` in `src/` code
|
||||
2. Remove all instantiations and injections
|
||||
3. Verify no methods are actually called
|
||||
4. Remove from DI containers (services.module.ts, bwdc.ts, main.ts)
|
||||
|
||||
Expected: Zero usage, straightforward removal.
|
||||
|
||||
### Step 7: Update WindowMain/TrayMain to Use StateServiceVNext
|
||||
|
||||
Update `jslib/electron/src/window.main.ts`:
|
||||
|
||||
```typescript
|
||||
// Change constructor to accept StateServiceVNext instead of StateService
|
||||
constructor(
|
||||
private stateService: StateServiceVNext, // Changed from StateService
|
||||
// ... other params
|
||||
) {}
|
||||
|
||||
// Update method calls to use StateServiceVNext interface
|
||||
async getWindowSettings(): Promise<any> {
|
||||
return await this.stateService.getWindowSettings();
|
||||
}
|
||||
|
||||
async setWindowSettings(settings: any): Promise<void> {
|
||||
await this.stateService.setWindowSettings(settings);
|
||||
}
|
||||
```
|
||||
|
||||
Update `jslib/electron/src/tray.main.ts` similarly:
|
||||
|
||||
```typescript
|
||||
constructor(
|
||||
private stateService: StateServiceVNext, // Changed from StateService
|
||||
// ... other params
|
||||
) {}
|
||||
|
||||
// Update method calls
|
||||
async getEnableTray(): Promise<boolean> {
|
||||
return await this.stateService.getEnableTray();
|
||||
}
|
||||
// ... etc for other tray settings
|
||||
```
|
||||
|
||||
**Required:** Add window/tray setting storage to StateServiceVNext:
|
||||
|
||||
- `getWindowSettings()` / `setWindowSettings()`
|
||||
- `getEnableTray()` / `getEnableMinimizeToTray()` / `getEnableCloseToTray()` / `getAlwaysShowDock()`
|
||||
|
||||
### Step 8: Update Service Registrations
|
||||
|
||||
**In `src/app/services/services.module.ts`:**
|
||||
|
||||
```typescript
|
||||
// Remove old services
|
||||
- import { StateService } from '@/services/state-service/state.service';
|
||||
- import { TokenService } from '@/jslib/common/src/services/token.service';
|
||||
- import { CryptoService } from '@/jslib/common/src/services/crypto.service';
|
||||
- import { EnvironmentService } from '@/jslib/common/src/services/environment.service';
|
||||
|
||||
// Add new services
|
||||
+ import { TokenService } from '@/services/token/token.service';
|
||||
+ import { EnvironmentService } from '@/services/environment/environment.service';
|
||||
|
||||
providers: [
|
||||
// Remove old StateService provider
|
||||
- { provide: StateService, useClass: StateService },
|
||||
|
||||
// Add new service providers
|
||||
+ { provide: TokenService, useClass: TokenService },
|
||||
+ { provide: EnvironmentService, useClass: EnvironmentService },
|
||||
|
||||
// Keep StateServiceVNext
|
||||
{ provide: StateServiceVNext, useClass: StateServiceVNextImplementation },
|
||||
]
|
||||
```
|
||||
|
||||
**In `src/bwdc.ts` (CLI):**
|
||||
|
||||
```typescript
|
||||
// Remove old service instantiations
|
||||
- this.stateService = new StateService(/* ... */);
|
||||
- this.cryptoService = new CryptoService(/* ... */);
|
||||
- this.tokenService = new TokenService(/* ... */);
|
||||
- this.environmentService = new EnvironmentService(this.stateService);
|
||||
|
||||
// Add new service instantiations
|
||||
+ this.tokenService = new TokenService(this.stateServiceVNext, secureStorageService);
|
||||
+ this.environmentService = new EnvironmentService(this.stateServiceVNext);
|
||||
```
|
||||
|
||||
**In `src/main.ts` (Electron):**
|
||||
|
||||
```typescript
|
||||
// Remove old service instantiations
|
||||
- this.stateService = new StateService(/* ... */);
|
||||
- this.cryptoService = new CryptoService(/* ... */);
|
||||
- this.tokenService = new TokenService(/* ... */);
|
||||
- this.environmentService = new EnvironmentService(this.stateService);
|
||||
|
||||
// Add new service instantiations
|
||||
+ this.tokenService = new TokenService(this.stateServiceVNext, secureStorageService);
|
||||
+ this.environmentService = new EnvironmentService(this.stateServiceVNext);
|
||||
|
||||
// Update WindowMain/TrayMain to use StateServiceVNext
|
||||
- this.windowMain = new WindowMain(this.stateService, /* ... */);
|
||||
- this.trayMain = new TrayMain(this.stateService, /* ... */);
|
||||
+ this.windowMain = new WindowMain(this.stateServiceVNext, /* ... */);
|
||||
+ this.trayMain = new TrayMain(this.stateServiceVNext, /* ... */);
|
||||
```
|
||||
|
||||
### Step 9: Update Import Statements
|
||||
|
||||
Update all files that import Token/Environment services:
|
||||
|
||||
**Files to update:**
|
||||
|
||||
- `src/services/api.service.ts` - Change TokenService import to DC version
|
||||
- `src/services/auth.service.ts` - Change EnvironmentService import to DC version
|
||||
- `src/services/sync.service.ts` - Change EnvironmentService import to DC version
|
||||
- `src/commands/config.command.ts` - Change EnvironmentService import to DC version
|
||||
- `src/app/app.component.ts` - Change TokenService import to DC version
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
|
||||
// After
|
||||
import { TokenService } from "@/abstractions/token.service";
|
||||
import { EnvironmentService } from "@/abstractions/environment.service";
|
||||
```
|
||||
|
||||
### Step 10: Delete Old StateService and jslib Services
|
||||
|
||||
**Delete these files (after all references removed):**
|
||||
|
||||
```bash
|
||||
# Old StateService implementations
|
||||
src/services/state-service/state.service.ts
|
||||
src/abstractions/state.service.ts
|
||||
jslib/common/src/services/state.service.ts
|
||||
jslib/common/src/abstractions/state.service.ts
|
||||
|
||||
# jslib Token/Crypto/Environment services
|
||||
jslib/common/src/services/token.service.ts
|
||||
jslib/common/src/abstractions/token.service.ts
|
||||
jslib/common/src/services/crypto.service.ts
|
||||
jslib/common/src/abstractions/crypto.service.ts
|
||||
jslib/common/src/services/environment.service.ts
|
||||
jslib/common/src/abstractions/environment.service.ts
|
||||
```
|
||||
|
||||
**Rename StateServiceVNext to StateService:**
|
||||
|
||||
```bash
|
||||
# Rename files
|
||||
mv src/services/state-service/state-vNext.service.ts src/services/state-service/state.service.ts
|
||||
mv src/services/state-service/state-vNext.service.spec.ts src/services/state-service/state.service.spec.ts
|
||||
mv src/abstractions/state-vNext.service.ts src/abstractions/state.service.ts
|
||||
|
||||
# Update all imports from StateServiceVNext to StateService
|
||||
# Find and replace: StateServiceVNext → StateService
|
||||
```
|
||||
|
||||
### Step 11: Update Tests
|
||||
|
||||
**Update existing tests that mock StateService:**
|
||||
|
||||
- Update mocks to use new StateService interface (flat key-value structure)
|
||||
- Remove mocks for Token/Crypto/Environment services where they inject old versions
|
||||
- Add mocks for new DC Token/Environment services
|
||||
|
||||
**Add new test files:**
|
||||
|
||||
- `src/services/token/token.service.spec.ts` (created in Step 2)
|
||||
- `src/services/environment/environment.service.spec.ts` (created in Step 3)
|
||||
- `src/utils/jwt.util.spec.ts` (JWT utility tests)
|
||||
|
||||
**Update integration tests:**
|
||||
|
||||
- Verify token storage/retrieval works correctly
|
||||
- Verify environment URL configuration persists
|
||||
- Verify window/tray settings persist in Electron app
|
||||
|
||||
## Verification Plan
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
npm test # Run all unit tests
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
|
||||
- All new service tests pass (TokenService, EnvironmentService, JWT util)
|
||||
- All existing tests pass with updated mocks
|
||||
- No test failures due to StateService removal
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```bash
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
|
||||
- LDAP sync tests pass
|
||||
- Authentication flow works correctly
|
||||
- Configuration persistence works
|
||||
|
||||
### Manual Testing - CLI
|
||||
|
||||
```bash
|
||||
# Build and run CLI
|
||||
npm run build:cli:watch
|
||||
node ./build-cli/bwdc.js --help
|
||||
|
||||
# Test authentication
|
||||
node ./build-cli/bwdc.js config server https://vault.bitwarden.com
|
||||
node ./build-cli/bwdc.js login --apikey
|
||||
|
||||
# Test sync
|
||||
node ./build-cli/bwdc.js config directory ldap
|
||||
node ./build-cli/bwdc.js config ldap.hostname ldap.example.com
|
||||
node ./build-cli/bwdc.js sync
|
||||
|
||||
# Test logout
|
||||
node ./build-cli/bwdc.js logout
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
- ✅ Server URL configuration persists
|
||||
- ✅ Login stores tokens correctly
|
||||
- ✅ Token refresh works automatically
|
||||
- ✅ Sync completes successfully
|
||||
- ✅ Logout clears tokens
|
||||
|
||||
### Manual Testing - Desktop App
|
||||
|
||||
```bash
|
||||
# Build and run desktop app
|
||||
npm run electron
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
- ✅ Window position/size persists across restarts
|
||||
- ✅ "Always on top" setting persists
|
||||
- ✅ Tray icon shows/hides based on settings
|
||||
- ✅ Minimize/close to tray works
|
||||
- ✅ Login/logout flow works
|
||||
- ✅ Sync functionality works
|
||||
- ✅ Custom server URL configuration works
|
||||
|
||||
### Migration Testing
|
||||
|
||||
**Test migration from existing installation:**
|
||||
|
||||
1. Install current production version
|
||||
2. Configure directory connection and run sync
|
||||
3. Install new version with StateService removal
|
||||
4. Launch app - verify automatic migration occurs
|
||||
5. Verify all settings preserved:
|
||||
- Directory configuration
|
||||
- Organization ID
|
||||
- Server URLs
|
||||
- Window/tray settings
|
||||
- Authentication tokens
|
||||
|
||||
**Expected:**
|
||||
|
||||
- ✅ Migration runs automatically on first launch
|
||||
- ✅ All user data preserved
|
||||
- ✅ No user action required
|
||||
- ✅ App functions identically to before
|
||||
|
||||
### Regression Testing
|
||||
|
||||
Run through all major workflows:
|
||||
|
||||
1. **Configuration**: Set up each directory type (LDAP, Entra, Google, Okta, OneLogin)
|
||||
2. **Authentication**: Login with API key, verify token refresh
|
||||
3. **Sync**: Full sync, incremental sync (delta tokens), detect changes via hash
|
||||
4. **Custom server**: Configure self-hosted Bitwarden server
|
||||
5. **Electron features**: Window management, tray behavior
|
||||
|
||||
**Expected:** No regressions in functionality.
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If critical issues discovered post-deployment:
|
||||
|
||||
1. **Revert commit** removing StateService
|
||||
2. **Keep StateServiceVNext in parallel** (already coexisting)
|
||||
3. **Debug issues** in development
|
||||
4. **Re-attempt removal** after fixes
|
||||
|
||||
**Risk Assessment:** Low - StateServiceVNext has been in production since Phase 1 PR merge, proven stable.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ All old StateService implementations deleted
|
||||
- ✅ StateServiceVNext renamed to StateService (becomes primary)
|
||||
- ✅ jslib TokenService, CryptoService, EnvironmentService deleted
|
||||
- ✅ DC-specific Token/Environment services implemented and tested
|
||||
- ✅ All unit tests pass
|
||||
- ✅ All integration tests pass
|
||||
- ✅ Manual testing shows no regressions
|
||||
- ✅ Migration from old state structure works automatically
|
||||
- ✅ WindowMain/TrayMain adapted to new StateService
|
||||
- ✅ Zero references to old StateService in codebase
|
||||
|
||||
## Next Steps (After Completion)
|
||||
|
||||
This unblocks **Phase 2: Remove Remaining jslib Code**:
|
||||
|
||||
- Delete unused jslib models (AccountData, AccountSettings, etc.)
|
||||
- Delete unused jslib services that referenced StateService
|
||||
- Clean up jslib/common folder of unused client code
|
||||
- Potentially merge remaining jslib code into src/ (flatten structure)
|
||||
|
||||
Estimated effort: 2-3 days for experienced developer familiar with codebase.
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
|
||||
@Directive()
|
||||
export class EnvironmentComponent {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
iconsUrl: string;
|
||||
identityUrl: string;
|
||||
apiUrl: string;
|
||||
webVaultUrl: string;
|
||||
notificationsUrl: string;
|
||||
baseUrl: string;
|
||||
showCustom = false;
|
||||
|
||||
constructor(
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected i18nService: I18nService,
|
||||
) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
this.apiUrl = urls.api || "";
|
||||
this.identityUrl = urls.identity || "";
|
||||
this.iconsUrl = urls.icons || "";
|
||||
this.notificationsUrl = urls.notifications || "";
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const resUrls = await this.environmentService.setUrls({
|
||||
base: this.baseUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
icons: this.iconsUrl,
|
||||
notifications: this.notificationsUrl,
|
||||
});
|
||||
|
||||
// re-set urls since service can change them, ex: prefixing https://
|
||||
this.baseUrl = resUrls.base;
|
||||
this.apiUrl = resUrls.api;
|
||||
this.identityUrl = resUrls.identity;
|
||||
this.webVaultUrl = resUrls.webVault;
|
||||
this.iconsUrl = resUrls.icons;
|
||||
this.notificationsUrl = resUrls.notifications;
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
|
||||
this.saved();
|
||||
}
|
||||
|
||||
toggleCustom() {
|
||||
this.showCustom = !this.showCustom;
|
||||
}
|
||||
|
||||
protected saved() {
|
||||
this.onSaved.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import { LOCALE_ID, NgModule } from "@angular/core";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||
import { ApiService } from "@/jslib/common/src/services/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/services/appId.service";
|
||||
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
import { StateService } from "@/jslib/common/src/services/state.service";
|
||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||
|
||||
import {
|
||||
SafeInjectionToken,
|
||||
SECURE_STORAGE,
|
||||
WINDOW,
|
||||
} from "../../../../src/app/services/injection-tokens";
|
||||
import { SafeProvider, safeProvider } from "../../../../src/app/services/safe-provider";
|
||||
|
||||
import { BroadcasterService } from "./broadcaster.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { ValidationService } from "./validation.service";
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
providers: [
|
||||
safeProvider({ provide: WINDOW, useValue: window }),
|
||||
safeProvider({
|
||||
provide: LOCALE_ID as SafeInjectionToken<string>,
|
||||
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
||||
deps: [I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider(ValidationService),
|
||||
safeProvider(ModalService),
|
||||
safeProvider({
|
||||
provide: AppIdServiceAbstraction,
|
||||
useClass: AppIdService,
|
||||
deps: [StorageServiceAbstraction],
|
||||
}),
|
||||
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
|
||||
safeProvider({
|
||||
provide: EnvironmentServiceAbstraction,
|
||||
useClass: EnvironmentService,
|
||||
deps: [StateServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TokenServiceAbstraction,
|
||||
useClass: TokenService,
|
||||
deps: [StateServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CryptoServiceAbstraction,
|
||||
useClass: CryptoService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ApiServiceAbstraction,
|
||||
useFactory: (
|
||||
tokenService: TokenServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
environmentService: EnvironmentServiceAbstraction,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
appIdService: AppIdServiceAbstraction,
|
||||
) =>
|
||||
new ApiService(
|
||||
tokenService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
appIdService,
|
||||
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
|
||||
),
|
||||
deps: [
|
||||
TokenServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BroadcasterServiceAbstraction,
|
||||
useClass: BroadcasterService,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateServiceAbstraction,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction,
|
||||
logService: LogService,
|
||||
stateMigrationService: StateMigrationServiceAbstraction,
|
||||
) =>
|
||||
new StateService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
new StateFactory(GlobalState, Account),
|
||||
),
|
||||
deps: [
|
||||
StorageServiceAbstraction,
|
||||
SECURE_STORAGE,
|
||||
LogService,
|
||||
StateMigrationServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateMigrationServiceAbstraction,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction,
|
||||
) =>
|
||||
new StateMigrationService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
new StateFactory(GlobalState, Account),
|
||||
),
|
||||
deps: [StorageServiceAbstraction, SECURE_STORAGE],
|
||||
}),
|
||||
] satisfies SafeProvider[],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { HashPurpose } from "../enums/hashPurpose";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
|
||||
import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse";
|
||||
import { ProfileProviderResponse } from "../models/response/profileProviderResponse";
|
||||
|
||||
export abstract class CryptoService {
|
||||
setKey: (key: SymmetricCryptoKey) => Promise<any>;
|
||||
setKeyHash: (keyHash: string) => Promise<void>;
|
||||
setEncKey: (encKey: string) => Promise<void>;
|
||||
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
|
||||
setOrgKeys: (
|
||||
orgs: ProfileOrganizationResponse[],
|
||||
providerOrgs: ProfileProviderOrganizationResponse[],
|
||||
) => Promise<void>;
|
||||
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
|
||||
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyHash: () => Promise<string>;
|
||||
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||
getPublicKey: () => Promise<ArrayBuffer>;
|
||||
getPrivateKey: () => Promise<ArrayBuffer>;
|
||||
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
||||
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
||||
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
||||
hasKey: () => Promise<boolean>;
|
||||
hasKeyInMemory: (userId?: string) => Promise<boolean>;
|
||||
hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<boolean>;
|
||||
hasEncKey: () => Promise<boolean>;
|
||||
clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise<any>;
|
||||
clearKeyHash: () => Promise<any>;
|
||||
clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearProviderKeys: (memoryOnly?: boolean) => Promise<any>;
|
||||
clearPinProtectedKey: () => Promise<any>;
|
||||
clearKeys: (userId?: string) => Promise<any>;
|
||||
toggleKey: () => Promise<any>;
|
||||
makeKey: (
|
||||
password: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeKeyFromPin: (
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
protectedKeyCs?: EncString,
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
|
||||
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
|
||||
makePinKey: (
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
|
||||
hashPassword: (
|
||||
password: string,
|
||||
key: SymmetricCryptoKey,
|
||||
hashPurpose?: HashPurpose,
|
||||
) => Promise<string>;
|
||||
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
remakeEncKey: (
|
||||
key: SymmetricCryptoKey,
|
||||
encKey?: SymmetricCryptoKey,
|
||||
) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncString>;
|
||||
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
|
||||
rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise<EncString>;
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
|
||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
|
||||
decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
|
||||
randomNumber: (min: number, max: number) => Promise<number>;
|
||||
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
}
|
||||
@@ -1,34 +1,2 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export type Urls = {
|
||||
base?: string;
|
||||
webVault?: string;
|
||||
api?: string;
|
||||
identity?: string;
|
||||
icons?: string;
|
||||
notifications?: string;
|
||||
events?: string;
|
||||
keyConnector?: string;
|
||||
};
|
||||
|
||||
export type PayPalConfig = {
|
||||
businessId?: string;
|
||||
buttonAction?: string;
|
||||
};
|
||||
|
||||
export abstract class EnvironmentService {
|
||||
urls: Observable<Urls>;
|
||||
|
||||
hasBaseUrl: () => boolean;
|
||||
getNotificationsUrl: () => string;
|
||||
getWebVaultUrl: () => string;
|
||||
getSendUrl: () => string;
|
||||
getIconsUrl: () => string;
|
||||
getApiUrl: () => string;
|
||||
getIdentityUrl: () => string;
|
||||
getEventsUrl: () => string;
|
||||
getKeyConnectorUrl: () => string;
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
getUrls: () => Urls;
|
||||
}
|
||||
// Stub file - re-exports DC EnvironmentService
|
||||
export { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service";
|
||||
|
||||
@@ -1,218 +1,2 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { ThemeType } from "../enums/themeType";
|
||||
import { UriMatchType } from "../enums/uriMatchType";
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { ProviderData } from "../models/data/providerData";
|
||||
import { Account } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||
import { StorageOptions } from "../models/domain/storageOptions";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { WindowState } from "../models/domain/windowState";
|
||||
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
accounts$: Observable<{ [userId: string]: T }>;
|
||||
activeAccount$: Observable<string>;
|
||||
|
||||
addAccount: (account: T) => Promise<void>;
|
||||
setActiveUser: (userId: string) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<void>;
|
||||
init: () => Promise<void>;
|
||||
|
||||
getAccessToken: (options?: StorageOptions) => Promise<string>;
|
||||
setAccessToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getAddEditCipherInfo: (options?: StorageOptions) => Promise<any>;
|
||||
setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
|
||||
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getApiKeyClientId: (options?: StorageOptions) => Promise<string>;
|
||||
setApiKeyClientId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getApiKeyClientSecret: (options?: StorageOptions) => Promise<string>;
|
||||
setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise<boolean>;
|
||||
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricLocked: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricLocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricText: (options?: StorageOptions) => Promise<string>;
|
||||
setBiometricText: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getBiometricUnlock: (options?: StorageOptions) => Promise<boolean>;
|
||||
setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
|
||||
getClearClipboard: (options?: StorageOptions) => Promise<number>;
|
||||
setClearClipboard: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getCollapsedGroupings: (options?: StorageOptions) => Promise<string[]>;
|
||||
setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise<void>;
|
||||
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
|
||||
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyB64: (options?: StorageOptions) => Promise<string>;
|
||||
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
|
||||
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getDecodedToken: (options?: StorageOptions) => Promise<any>;
|
||||
setDecodedToken: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setDecryptedCryptoSymmetricKey: (
|
||||
value: SymmetricCryptoKey,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDecryptedOrganizationKeys: (
|
||||
options?: StorageOptions,
|
||||
) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
setDecryptedOrganizationKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
|
||||
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
|
||||
setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedProviderKeys: (options?: StorageOptions) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
setDecryptedProviderKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
|
||||
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
||||
getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableAutoTotpCopy: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableBadgeCounter: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDisableGa: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableAlwaysOnTop: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableMinimizeToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableStartToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise<any>;
|
||||
setEncryptedOrganizationKeys: (
|
||||
value: Map<string, SymmetricCryptoKey>,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedPrivateKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEncryptedProviderKeys: (options?: StorageOptions) => Promise<any>;
|
||||
setEncryptedProviderKeys: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getEntityId: (options?: StorageOptions) => Promise<string>;
|
||||
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
||||
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
||||
getEquivalentDomains: (options?: StorageOptions) => Promise<any>;
|
||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getForcePasswordReset: (options?: StorageOptions) => Promise<boolean>;
|
||||
setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getInstalledVersion: (options?: StorageOptions) => Promise<string>;
|
||||
setInstalledVersion: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
||||
getKdfIterations: (options?: StorageOptions) => Promise<number>;
|
||||
setKdfIterations: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getKdfType: (options?: StorageOptions) => Promise<KdfType>;
|
||||
setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>;
|
||||
getKeyHash: (options?: StorageOptions) => Promise<string>;
|
||||
setKeyHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastActive: (options?: StorageOptions) => Promise<number>;
|
||||
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLegacyEtmKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setLegacyEtmKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
|
||||
getLocalData: (options?: StorageOptions) => Promise<any>;
|
||||
setLocalData: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLocale: (options?: StorageOptions) => Promise<string>;
|
||||
setLocale: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLoginRedirect: (options?: StorageOptions) => Promise<any>;
|
||||
setLoginRedirect: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getMainWindowSize: (options?: StorageOptions) => Promise<number>;
|
||||
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>;
|
||||
setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise<void>;
|
||||
getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise<boolean>;
|
||||
setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise<string>;
|
||||
setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
|
||||
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
|
||||
setOrganizations: (
|
||||
value: { [id: string]: OrganizationData },
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getUsernameGenerationOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getGeneratorOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setGeneratorOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getProtectedPin: (options?: StorageOptions) => Promise<string>;
|
||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
|
||||
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
|
||||
getPublicKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
|
||||
setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
|
||||
getRefreshToken: (options?: StorageOptions) => Promise<string>;
|
||||
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSettings: (options?: StorageOptions) => Promise<any>;
|
||||
setSettings: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoCodeVerifier: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoOrgIdentifier: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoState: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||
getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getStateVersion: () => Promise<number>;
|
||||
setStateVersion: (value: number) => Promise<void>;
|
||||
getWindow: () => Promise<WindowState>;
|
||||
setWindow: (value: WindowState) => Promise<void>;
|
||||
}
|
||||
// Stub file - re-exports DC StateService
|
||||
export { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export abstract class StateMigrationService {
|
||||
needsMigration: () => Promise<boolean>;
|
||||
migrate: () => Promise<void>;
|
||||
}
|
||||
@@ -1,32 +1,2 @@
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export abstract class TokenService {
|
||||
setTokens: (
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret: [string, string],
|
||||
) => Promise<any>;
|
||||
setToken: (token: string) => Promise<any>;
|
||||
getToken: () => Promise<string>;
|
||||
setRefreshToken: (refreshToken: string) => Promise<any>;
|
||||
getRefreshToken: () => Promise<string>;
|
||||
setClientId: (clientId: string) => Promise<any>;
|
||||
getClientId: () => Promise<string>;
|
||||
setClientSecret: (clientSecret: string) => Promise<any>;
|
||||
getClientSecret: () => Promise<string>;
|
||||
setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise<any>;
|
||||
getTwoFactorToken: () => Promise<string>;
|
||||
clearTwoFactorToken: () => Promise<any>;
|
||||
clearToken: (userId?: string) => Promise<any>;
|
||||
decodeToken: (token?: string) => any;
|
||||
getTokenExpirationDate: () => Promise<Date>;
|
||||
tokenSecondsRemaining: (offsetSeconds?: number) => Promise<number>;
|
||||
tokenNeedsRefresh: (minutes?: number) => Promise<boolean>;
|
||||
getUserId: () => Promise<string>;
|
||||
getEmail: () => Promise<string>;
|
||||
getEmailVerified: () => Promise<boolean>;
|
||||
getName: () => Promise<string>;
|
||||
getPremium: () => Promise<boolean>;
|
||||
getIssuer: () => Promise<string>;
|
||||
getIsExternal: () => Promise<boolean>;
|
||||
}
|
||||
// Stub file - re-exports DC TokenService
|
||||
export { TokenService } from "@/src/abstractions/token.service";
|
||||
|
||||
@@ -3,5 +3,6 @@ export enum StateVersion {
|
||||
Two = 2, // Move to a typed State object
|
||||
Three = 3, // Fix migration of users' premium status
|
||||
Four = 4, // Fix 'Never Lock' option by removing stale data
|
||||
Latest = Four,
|
||||
Five = 5, // New state service implementation
|
||||
Latest = Five,
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Account } from "../models/domain/account";
|
||||
|
||||
export class AccountFactory<T extends Account = Account> {
|
||||
private accountConstructor: new (init: Partial<T>) => T;
|
||||
|
||||
constructor(accountConstructor: new (init: Partial<T>) => T) {
|
||||
this.accountConstructor = accountConstructor;
|
||||
}
|
||||
|
||||
create(args: Partial<T>) {
|
||||
return new this.accountConstructor(args);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Account } from "../models/domain/account";
|
||||
import { GlobalState } from "../models/domain/globalState";
|
||||
|
||||
import { AccountFactory } from "./accountFactory";
|
||||
import { GlobalStateFactory } from "./globalStateFactory";
|
||||
|
||||
export class StateFactory<
|
||||
TGlobal extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> {
|
||||
private globalStateFactory: GlobalStateFactory<TGlobal>;
|
||||
private accountFactory: AccountFactory<TAccount>;
|
||||
|
||||
constructor(
|
||||
globalStateConstructor: new (init: Partial<TGlobal>) => TGlobal,
|
||||
accountConstructor: new (init: Partial<TAccount>) => TAccount,
|
||||
) {
|
||||
this.globalStateFactory = new GlobalStateFactory(globalStateConstructor);
|
||||
this.accountFactory = new AccountFactory(accountConstructor);
|
||||
}
|
||||
|
||||
createGlobal(args: Partial<TGlobal>): TGlobal {
|
||||
return this.globalStateFactory.create(args);
|
||||
}
|
||||
|
||||
createAccount(args: Partial<TAccount>): TAccount {
|
||||
return this.accountFactory.create(args);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { AuthenticationStatus } from "../../enums/authenticationStatus";
|
||||
import { KdfType } from "../../enums/kdfType";
|
||||
import { UriMatchType } from "../../enums/uriMatchType";
|
||||
import { OrganizationData } from "../data/organizationData";
|
||||
import { ProviderData } from "../data/providerData";
|
||||
|
||||
import { EncString } from "./encString";
|
||||
import { EnvironmentUrls } from "./environmentUrls";
|
||||
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
decrypted?: TDecrypted;
|
||||
}
|
||||
|
||||
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: { [id: string]: TEncrypted };
|
||||
decrypted?: TDecrypted[];
|
||||
}
|
||||
|
||||
export class AccountData {
|
||||
ciphers?: any = new DataEncryptionPair<any, any>();
|
||||
folders?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
|
||||
localData?: any;
|
||||
sends?: any = new DataEncryptionPair<any, any>();
|
||||
collections?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
|
||||
policies?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
|
||||
passwordGenerationHistory?: EncryptionPair<any[], any[]> = new EncryptionPair<any[], any[]>();
|
||||
addEditCipherInfo?: any;
|
||||
eventCollection?: any[];
|
||||
organizations?: { [id: string]: OrganizationData };
|
||||
providers?: { [id: string]: ProviderData };
|
||||
}
|
||||
|
||||
export class AccountKeys {
|
||||
cryptoMasterKey?: SymmetricCryptoKey;
|
||||
cryptoMasterKeyAuto?: string;
|
||||
cryptoMasterKeyB64?: string;
|
||||
cryptoMasterKeyBiometric?: string;
|
||||
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
organizationKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
|
||||
any,
|
||||
Map<string, SymmetricCryptoKey>
|
||||
>();
|
||||
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
|
||||
any,
|
||||
Map<string, SymmetricCryptoKey>
|
||||
>();
|
||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||
legacyEtmKey?: SymmetricCryptoKey;
|
||||
publicKey?: ArrayBuffer;
|
||||
apiKeyClientSecret?: string;
|
||||
}
|
||||
|
||||
export class AccountProfile {
|
||||
apiKeyClientId?: string;
|
||||
authenticationStatus?: AuthenticationStatus;
|
||||
convertAccountToKeyConnector?: boolean;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
entityId?: string;
|
||||
entityType?: string;
|
||||
everBeenUnlocked?: boolean;
|
||||
forcePasswordReset?: boolean;
|
||||
hasPremiumPersonally?: boolean;
|
||||
lastSync?: string;
|
||||
userId?: string;
|
||||
usesKeyConnector?: boolean;
|
||||
keyHash?: string;
|
||||
kdfIterations?: number;
|
||||
kdfType?: KdfType;
|
||||
}
|
||||
|
||||
export class AccountSettings {
|
||||
autoConfirmFingerPrints?: boolean;
|
||||
autoFillOnPageLoadDefault?: boolean;
|
||||
biometricLocked?: boolean;
|
||||
biometricUnlock?: boolean;
|
||||
clearClipboard?: number;
|
||||
collapsedGroupings?: string[];
|
||||
defaultUriMatch?: UriMatchType;
|
||||
disableAddLoginNotification?: boolean;
|
||||
disableAutoBiometricsPrompt?: boolean;
|
||||
disableAutoTotpCopy?: boolean;
|
||||
disableBadgeCounter?: boolean;
|
||||
disableChangedPasswordNotification?: boolean;
|
||||
disableContextMenuItem?: boolean;
|
||||
disableGa?: boolean;
|
||||
dontShowCardsCurrentTab?: boolean;
|
||||
dontShowIdentitiesCurrentTab?: boolean;
|
||||
enableAlwaysOnTop?: boolean;
|
||||
enableAutoFillOnPageLoad?: boolean;
|
||||
enableBiometric?: boolean;
|
||||
enableFullWidth?: boolean;
|
||||
enableGravitars?: boolean;
|
||||
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
|
||||
equivalentDomains?: any;
|
||||
minimizeOnCopyToClipboard?: boolean;
|
||||
neverDomains?: { [id: string]: any };
|
||||
passwordGenerationOptions?: any;
|
||||
usernameGenerationOptions?: any;
|
||||
generatorOptions?: any;
|
||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
protectedPin?: string;
|
||||
settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly
|
||||
vaultTimeout?: number;
|
||||
vaultTimeoutAction?: string = "lock";
|
||||
}
|
||||
|
||||
export class AccountTokens {
|
||||
accessToken?: string;
|
||||
decodedToken?: any;
|
||||
refreshToken?: string;
|
||||
securityStamp?: string;
|
||||
}
|
||||
|
||||
export class Account {
|
||||
data?: AccountData = new AccountData();
|
||||
keys?: AccountKeys = new AccountKeys();
|
||||
profile?: AccountProfile = new AccountProfile();
|
||||
settings?: AccountSettings = new AccountSettings();
|
||||
tokens?: AccountTokens = new AccountTokens();
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
Object.assign(this, {
|
||||
data: {
|
||||
...new AccountData(),
|
||||
...init?.data,
|
||||
},
|
||||
keys: {
|
||||
...new AccountKeys(),
|
||||
...init?.keys,
|
||||
},
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...init?.profile,
|
||||
},
|
||||
settings: {
|
||||
...new AccountSettings(),
|
||||
...init?.settings,
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...init?.tokens,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { View } from "../view/view";
|
||||
|
||||
import { EncString } from "./encString";
|
||||
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||
|
||||
export default class Domain {
|
||||
protected buildDomainModel<D extends Domain>(
|
||||
domain: D,
|
||||
dataObj: any,
|
||||
map: any,
|
||||
notEncList: any[] = [],
|
||||
) {
|
||||
for (const prop in map) {
|
||||
// eslint-disable-next-line
|
||||
if (!map.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objProp = dataObj[map[prop] || prop];
|
||||
if (notEncList.indexOf(prop) > -1) {
|
||||
(domain as any)[prop] = objProp ? objProp : null;
|
||||
} else {
|
||||
(domain as any)[prop] = objProp ? new EncString(objProp) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
protected buildDataModel<D extends Domain>(
|
||||
domain: D,
|
||||
dataObj: any,
|
||||
map: any,
|
||||
notEncStringList: any[] = [],
|
||||
) {
|
||||
for (const prop in map) {
|
||||
// eslint-disable-next-line
|
||||
if (!map.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objProp = (domain as any)[map[prop] || prop];
|
||||
if (notEncStringList.indexOf(prop) > -1) {
|
||||
(dataObj as any)[prop] = objProp != null ? objProp : null;
|
||||
} else {
|
||||
(dataObj as any)[prop] = objProp != null ? (objProp as EncString).encryptedString : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async decryptObj<T extends View>(
|
||||
viewModel: T,
|
||||
map: any,
|
||||
orgId: string,
|
||||
key: SymmetricCryptoKey = null,
|
||||
): Promise<T> {
|
||||
const promises = [];
|
||||
const self: any = this;
|
||||
|
||||
for (const prop in map) {
|
||||
// eslint-disable-next-line
|
||||
if (!map.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(function (theProp) {
|
||||
const p = Promise.resolve()
|
||||
.then(() => {
|
||||
const mapProp = map[theProp] || theProp;
|
||||
if (self[mapProp]) {
|
||||
return self[mapProp].decrypt(orgId, key);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.then((val: any) => {
|
||||
(viewModel as any)[theProp] = val;
|
||||
});
|
||||
promises.push(p);
|
||||
})(prop);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
return viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { EncryptionType } from "../../enums/encryptionType";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||
|
||||
export class EncString {
|
||||
encryptedString?: string;
|
||||
encryptionType?: EncryptionType;
|
||||
decryptedValue?: string;
|
||||
data?: string;
|
||||
iv?: string;
|
||||
mac?: string;
|
||||
|
||||
constructor(
|
||||
encryptedStringOrType: string | EncryptionType,
|
||||
data?: string,
|
||||
iv?: string,
|
||||
mac?: string,
|
||||
) {
|
||||
if (data != null) {
|
||||
// data and header
|
||||
const encType = encryptedStringOrType as EncryptionType;
|
||||
|
||||
if (iv != null) {
|
||||
this.encryptedString = encType + "." + iv + "|" + data;
|
||||
} else {
|
||||
this.encryptedString = encType + "." + data;
|
||||
}
|
||||
|
||||
// mac
|
||||
if (mac != null) {
|
||||
this.encryptedString += "|" + mac;
|
||||
}
|
||||
|
||||
this.encryptionType = encType;
|
||||
this.data = data;
|
||||
this.iv = iv;
|
||||
this.mac = mac;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.encryptedString = encryptedStringOrType as string;
|
||||
if (!this.encryptedString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headerPieces = this.encryptedString.split(".");
|
||||
let encPieces: string[] = null;
|
||||
|
||||
if (headerPieces.length === 2) {
|
||||
try {
|
||||
this.encryptionType = parseInt(headerPieces[0], null);
|
||||
encPieces = headerPieces[1].split("|");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
encPieces = this.encryptedString.split("|");
|
||||
this.encryptionType =
|
||||
encPieces.length === 3
|
||||
? EncryptionType.AesCbc128_HmacSha256_B64
|
||||
: EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
|
||||
switch (this.encryptionType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if (encPieces.length !== 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iv = encPieces[0];
|
||||
this.data = encPieces[1];
|
||||
this.mac = encPieces[2];
|
||||
break;
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if (encPieces.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iv = encPieces[0];
|
||||
this.data = encPieces[1];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
if (encPieces.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = encPieces[0];
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise<string> {
|
||||
if (this.decryptedValue != null) {
|
||||
return this.decryptedValue;
|
||||
}
|
||||
|
||||
let cryptoService: CryptoService;
|
||||
const containerService = (Utils.global as any).bitwardenContainerService;
|
||||
if (containerService) {
|
||||
cryptoService = containerService.getCryptoService();
|
||||
} else {
|
||||
throw new Error("global bitwardenContainerService not initialized.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (key == null) {
|
||||
key = await cryptoService.getOrgKey(orgId);
|
||||
}
|
||||
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
|
||||
} catch {
|
||||
this.decryptedValue = "[error: cannot decrypt]";
|
||||
}
|
||||
return this.decryptedValue;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,5 @@ export class EnvironmentUrls {
|
||||
base: string = null;
|
||||
api: string = null;
|
||||
identity: string = null;
|
||||
icons: string = null;
|
||||
notifications: string = null;
|
||||
events: string = null;
|
||||
webVault: string = null;
|
||||
keyConnector: string = null;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Account } from "./account";
|
||||
import { GlobalState } from "./globalState";
|
||||
|
||||
export class State<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> {
|
||||
accounts: { [userId: string]: TAccount } = {};
|
||||
globals: TGlobalState;
|
||||
activeUserId: string;
|
||||
authenticatedAccounts: string[] = [];
|
||||
accountActivity: { [userId: string]: number } = {};
|
||||
|
||||
constructor(globals: TGlobalState) {
|
||||
this.globals = globals;
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,10 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
throw new Error("Invalid response received when refreshing api token");
|
||||
}
|
||||
|
||||
await this.tokenService.setToken(response.accessToken);
|
||||
await this.tokenService.setTokens(response.accessToken, response.refreshToken, [
|
||||
clientId,
|
||||
clientSecret,
|
||||
]);
|
||||
}
|
||||
|
||||
private async send(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
|
||||
export class ContainerService {
|
||||
constructor(private cryptoService: CryptoService) {}
|
||||
|
||||
// deprecated, use attachToGlobal instead
|
||||
attachToWindow(win: any) {
|
||||
this.attachToGlobal(win);
|
||||
}
|
||||
|
||||
attachToGlobal(global: any) {
|
||||
if (!global.bitwardenContainerService) {
|
||||
global.bitwardenContainerService = this;
|
||||
}
|
||||
}
|
||||
|
||||
getCryptoService(): CryptoService {
|
||||
return this.cryptoService;
|
||||
}
|
||||
}
|
||||
@@ -1,971 +0,0 @@
|
||||
import * as bigInt from "big-integer";
|
||||
|
||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EncryptionType } from "../enums/encryptionType";
|
||||
import { HashPurpose } from "../enums/hashPurpose";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { EEFLongWordList } from "../misc/wordlist";
|
||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { EncryptedObject } from "../models/domain/encryptedObject";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
|
||||
import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse";
|
||||
import { ProfileProviderResponse } from "../models/response/profileProviderResponse";
|
||||
|
||||
export class CryptoService implements CryptoServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
protected platformUtilService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService,
|
||||
) {}
|
||||
|
||||
async setKey(key: SymmetricCryptoKey, userId?: string): Promise<any> {
|
||||
await this.stateService.setCryptoMasterKey(key, { userId: userId });
|
||||
await this.storeKey(key, userId);
|
||||
}
|
||||
|
||||
async setKeyHash(keyHash: string): Promise<void> {
|
||||
await this.stateService.setKeyHash(keyHash);
|
||||
}
|
||||
|
||||
async setEncKey(encKey: string): Promise<void> {
|
||||
if (encKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(null);
|
||||
await this.stateService.setEncryptedCryptoSymmetricKey(encKey);
|
||||
}
|
||||
|
||||
async setEncPrivateKey(encPrivateKey: string): Promise<void> {
|
||||
if (encPrivateKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedPrivateKey(null);
|
||||
await this.stateService.setEncryptedPrivateKey(encPrivateKey);
|
||||
}
|
||||
|
||||
async setOrgKeys(
|
||||
orgs: ProfileOrganizationResponse[],
|
||||
providerOrgs: ProfileProviderOrganizationResponse[],
|
||||
): Promise<void> {
|
||||
const orgKeys: any = {};
|
||||
orgs.forEach((org) => {
|
||||
orgKeys[org.id] = org.key;
|
||||
});
|
||||
|
||||
for (const providerOrg of providerOrgs) {
|
||||
// Convert provider encrypted keys to user encrypted.
|
||||
const providerKey = await this.getProviderKey(providerOrg.providerId);
|
||||
const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey);
|
||||
orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedOrganizationKeys(null);
|
||||
return await this.stateService.setEncryptedOrganizationKeys(orgKeys);
|
||||
}
|
||||
|
||||
async setProviderKeys(providers: ProfileProviderResponse[]): Promise<void> {
|
||||
const providerKeys: any = {};
|
||||
providers.forEach((provider) => {
|
||||
providerKeys[provider.id] = provider.key;
|
||||
});
|
||||
|
||||
await this.stateService.setDecryptedProviderKeys(null);
|
||||
return await this.stateService.setEncryptedProviderKeys(providerKeys);
|
||||
}
|
||||
|
||||
async getKey(keySuffix?: KeySuffixOptions, userId?: string): Promise<SymmetricCryptoKey> {
|
||||
const inMemoryKey = await this.stateService.getCryptoMasterKey({ userId: userId });
|
||||
|
||||
if (inMemoryKey != null) {
|
||||
return inMemoryKey;
|
||||
}
|
||||
|
||||
keySuffix ||= KeySuffixOptions.Auto;
|
||||
const symmetricKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||
|
||||
if (symmetricKey != null) {
|
||||
// TODO: Refactor here so get key doesn't also set key
|
||||
this.setKey(symmetricKey, userId);
|
||||
}
|
||||
|
||||
return symmetricKey;
|
||||
}
|
||||
|
||||
async getKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: string,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const key = await this.retrieveKeyFromStorage(keySuffix, userId);
|
||||
if (key != null) {
|
||||
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
|
||||
|
||||
if (!(await this.validateKey(symmetricKey))) {
|
||||
this.logService.warning("Wrong key, throwing away stored key");
|
||||
await this.clearSecretKeyStore(userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return symmetricKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getKeyHash(): Promise<string> {
|
||||
return await this.stateService.getKeyHash();
|
||||
}
|
||||
|
||||
async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise<boolean> {
|
||||
const storedKeyHash = await this.getKeyHash();
|
||||
if (masterPassword != null && storedKeyHash != null) {
|
||||
const localKeyHash = await this.hashPassword(
|
||||
masterPassword,
|
||||
key,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
if (localKeyHash != null && storedKeyHash === localKeyHash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated
|
||||
const serverKeyHash = await this.hashPassword(
|
||||
masterPassword,
|
||||
key,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
if (serverKeyHash != null && storedKeyHash === serverKeyHash) {
|
||||
await this.setKeyHash(localKeyHash);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@sequentialize(() => "getEncKey")
|
||||
getEncKey(key: SymmetricCryptoKey = null): Promise<SymmetricCryptoKey> {
|
||||
return this.getEncKeyHelper(key);
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<ArrayBuffer> {
|
||||
const inMemoryPublicKey = await this.stateService.getPublicKey();
|
||||
if (inMemoryPublicKey != null) {
|
||||
return inMemoryPublicKey;
|
||||
}
|
||||
|
||||
const privateKey = await this.getPrivateKey();
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
await this.stateService.setPublicKey(publicKey);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<ArrayBuffer> {
|
||||
const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey();
|
||||
if (decryptedPrivateKey != null) {
|
||||
return decryptedPrivateKey;
|
||||
}
|
||||
|
||||
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
|
||||
if (encPrivateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null);
|
||||
await this.stateService.setDecryptedPrivateKey(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
if (publicKey === null) {
|
||||
throw new Error("No public key available.");
|
||||
}
|
||||
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
|
||||
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
|
||||
keyFingerprint,
|
||||
userId,
|
||||
32,
|
||||
"sha256",
|
||||
);
|
||||
return this.hashPhrase(userFingerprint);
|
||||
}
|
||||
|
||||
@sequentialize(() => "getOrgKeys")
|
||||
async getOrgKeys(): Promise<Map<string, SymmetricCryptoKey>> {
|
||||
const orgKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
const decryptedOrganizationKeys = await this.stateService.getDecryptedOrganizationKeys();
|
||||
if (decryptedOrganizationKeys != null && decryptedOrganizationKeys.size > 0) {
|
||||
return decryptedOrganizationKeys;
|
||||
}
|
||||
|
||||
const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys();
|
||||
if (encOrgKeys == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let setKey = false;
|
||||
|
||||
for (const orgId in encOrgKeys) {
|
||||
// eslint-disable-next-line
|
||||
if (!encOrgKeys.hasOwnProperty(orgId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decValue = await this.rsaDecrypt(encOrgKeys[orgId]);
|
||||
orgKeys.set(orgId, new SymmetricCryptoKey(decValue));
|
||||
setKey = true;
|
||||
}
|
||||
|
||||
if (setKey) {
|
||||
await this.stateService.setDecryptedOrganizationKeys(orgKeys);
|
||||
}
|
||||
|
||||
return orgKeys;
|
||||
}
|
||||
|
||||
async getOrgKey(orgId: string): Promise<SymmetricCryptoKey> {
|
||||
if (orgId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgKeys = await this.getOrgKeys();
|
||||
if (orgKeys == null || !orgKeys.has(orgId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return orgKeys.get(orgId);
|
||||
}
|
||||
|
||||
@sequentialize(() => "getProviderKeys")
|
||||
async getProviderKeys(): Promise<Map<string, SymmetricCryptoKey>> {
|
||||
const providerKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
const decryptedProviderKeys = await this.stateService.getDecryptedProviderKeys();
|
||||
if (decryptedProviderKeys != null && decryptedProviderKeys.size > 0) {
|
||||
return decryptedProviderKeys;
|
||||
}
|
||||
|
||||
const encProviderKeys = await this.stateService.getEncryptedProviderKeys();
|
||||
if (encProviderKeys == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let setKey = false;
|
||||
|
||||
for (const orgId in encProviderKeys) {
|
||||
// eslint-disable-next-line
|
||||
if (!encProviderKeys.hasOwnProperty(orgId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decValue = await this.rsaDecrypt(encProviderKeys[orgId]);
|
||||
providerKeys.set(orgId, new SymmetricCryptoKey(decValue));
|
||||
setKey = true;
|
||||
}
|
||||
|
||||
if (setKey) {
|
||||
await this.stateService.setDecryptedProviderKeys(providerKeys);
|
||||
}
|
||||
|
||||
return providerKeys;
|
||||
}
|
||||
|
||||
async getProviderKey(providerId: string): Promise<SymmetricCryptoKey> {
|
||||
if (providerId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerKeys = await this.getProviderKeys();
|
||||
if (providerKeys == null || !providerKeys.has(providerId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return providerKeys.get(providerId);
|
||||
}
|
||||
|
||||
async hasKey(): Promise<boolean> {
|
||||
return (
|
||||
(await this.hasKeyInMemory()) ||
|
||||
(await this.hasKeyStored(KeySuffixOptions.Auto)) ||
|
||||
(await this.hasKeyStored(KeySuffixOptions.Biometric))
|
||||
);
|
||||
}
|
||||
|
||||
async hasKeyInMemory(userId?: string): Promise<boolean> {
|
||||
return (await this.stateService.getCryptoMasterKey({ userId: userId })) != null;
|
||||
}
|
||||
|
||||
async hasKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
||||
switch (keySuffix) {
|
||||
case KeySuffixOptions.Auto:
|
||||
return (await this.stateService.getCryptoMasterKeyAuto({ userId: userId })) != null;
|
||||
case KeySuffixOptions.Biometric:
|
||||
return (await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId })) === true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hasEncKey(): Promise<boolean> {
|
||||
return (await this.stateService.getEncryptedCryptoSymmetricKey()) != null;
|
||||
}
|
||||
|
||||
async clearKey(clearSecretStorage = true, userId?: string): Promise<any> {
|
||||
await this.stateService.setCryptoMasterKey(null, { userId: userId });
|
||||
await this.stateService.setLegacyEtmKey(null, { userId: userId });
|
||||
if (clearSecretStorage) {
|
||||
await this.clearSecretKeyStore(userId);
|
||||
}
|
||||
}
|
||||
|
||||
async clearStoredKey(keySuffix: KeySuffixOptions) {
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null);
|
||||
} else {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null);
|
||||
}
|
||||
}
|
||||
|
||||
async clearKeyHash(userId?: string): Promise<any> {
|
||||
return await this.stateService.setKeyHash(null, { userId: userId });
|
||||
}
|
||||
|
||||
async clearEncKey(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedCryptoSymmetricKey(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise<any> {
|
||||
const keysToClear: Promise<void>[] = [
|
||||
this.stateService.setDecryptedPrivateKey(null, { userId: userId }),
|
||||
this.stateService.setPublicKey(null, { userId: userId }),
|
||||
];
|
||||
if (!memoryOnly) {
|
||||
keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId }));
|
||||
}
|
||||
return Promise.all(keysToClear);
|
||||
}
|
||||
|
||||
async clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedProviderKeys(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedProviderKeys(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearPinProtectedKey(userId?: string): Promise<any> {
|
||||
return await this.stateService.setEncryptedPinProtected(null, { userId: userId });
|
||||
}
|
||||
|
||||
async clearKeys(userId?: string): Promise<any> {
|
||||
await this.clearKey(true, userId);
|
||||
await this.clearKeyHash(userId);
|
||||
await this.clearOrgKeys(false, userId);
|
||||
await this.clearProviderKeys(false, userId);
|
||||
await this.clearEncKey(false, userId);
|
||||
await this.clearKeyPair(false, userId);
|
||||
await this.clearPinProtectedKey(userId);
|
||||
}
|
||||
|
||||
async toggleKey(): Promise<any> {
|
||||
const key = await this.getKey();
|
||||
|
||||
await this.setKey(key);
|
||||
}
|
||||
|
||||
async makeKey(
|
||||
password: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
let key: ArrayBuffer = null;
|
||||
if (kdf == null || kdf === KdfType.PBKDF2_SHA256) {
|
||||
if (kdfIterations == null) {
|
||||
kdfIterations = 5000;
|
||||
} else if (kdfIterations < 5000) {
|
||||
throw new Error("PBKDF2 iteration minimum is 5000.");
|
||||
}
|
||||
key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfIterations);
|
||||
} else {
|
||||
throw new Error("Unknown Kdf.");
|
||||
}
|
||||
return new SymmetricCryptoKey(key);
|
||||
}
|
||||
|
||||
async makeKeyFromPin(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
protectedKeyCs: EncString = null,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
if (protectedKeyCs == null) {
|
||||
const pinProtectedKey = await this.stateService.getEncryptedPinProtected();
|
||||
if (pinProtectedKey == null) {
|
||||
throw new Error("No PIN protected key found.");
|
||||
}
|
||||
protectedKeyCs = new EncString(pinProtectedKey);
|
||||
}
|
||||
const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations);
|
||||
const decKey = await this.decryptToBytes(protectedKeyCs, pinKey);
|
||||
return new SymmetricCryptoKey(decKey);
|
||||
}
|
||||
|
||||
async makeShareKey(): Promise<[EncString, SymmetricCryptoKey]> {
|
||||
const shareKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
const publicKey = await this.getPublicKey();
|
||||
const encShareKey = await this.rsaEncrypt(shareKey, publicKey);
|
||||
return [encShareKey, new SymmetricCryptoKey(shareKey)];
|
||||
}
|
||||
|
||||
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
|
||||
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
|
||||
const privateEnc = await this.encrypt(keyPair[1], key);
|
||||
return [publicB64, privateEnc];
|
||||
}
|
||||
|
||||
async makePinKey(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations);
|
||||
return await this.stretchKey(pinKey);
|
||||
}
|
||||
|
||||
async makeSendKey(keyMaterial: ArrayBuffer): Promise<SymmetricCryptoKey> {
|
||||
const sendKey = await this.cryptoFunctionService.hkdf(
|
||||
keyMaterial,
|
||||
"bitwarden-send",
|
||||
"send",
|
||||
64,
|
||||
"sha256",
|
||||
);
|
||||
return new SymmetricCryptoKey(sendKey);
|
||||
}
|
||||
|
||||
async hashPassword(
|
||||
password: string,
|
||||
key: SymmetricCryptoKey,
|
||||
hashPurpose?: HashPurpose,
|
||||
): Promise<string> {
|
||||
if (key == null) {
|
||||
key = await this.getKey();
|
||||
}
|
||||
if (password == null || key == null) {
|
||||
throw new Error("Invalid parameters.");
|
||||
}
|
||||
|
||||
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
|
||||
const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations);
|
||||
return Utils.fromBufferToB64(hash);
|
||||
}
|
||||
|
||||
async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
const theKey = await this.getKeyForEncryption(key);
|
||||
const encKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
return this.buildEncKey(theKey, encKey);
|
||||
}
|
||||
|
||||
async remakeEncKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
if (encKey == null) {
|
||||
encKey = await this.getEncKey();
|
||||
}
|
||||
return this.buildEncKey(key, encKey.key);
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (plainValue == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
let plainBuf: ArrayBuffer;
|
||||
if (typeof plainValue === "string") {
|
||||
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
|
||||
} else {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
|
||||
const encObj = await this.aesEncrypt(plainBuf, key);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
let macLen = 0;
|
||||
if (encValue.mac != null) {
|
||||
macLen = encValue.mac.byteLength;
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
||||
encBytes.set([encValue.key.encType]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
if (encValue.mac != null) {
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes.buffer);
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
if (publicKey == null) {
|
||||
throw new Error("Public key unavailable.");
|
||||
}
|
||||
|
||||
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
|
||||
}
|
||||
|
||||
async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const headerPieces = encValue.split(".");
|
||||
let encType: EncryptionType = null;
|
||||
let encPieces: string[];
|
||||
|
||||
if (headerPieces.length === 1) {
|
||||
encType = EncryptionType.Rsa2048_OaepSha256_B64;
|
||||
encPieces = [headerPieces[0]];
|
||||
} else if (headerPieces.length === 2) {
|
||||
try {
|
||||
encType = parseInt(headerPieces[0], null);
|
||||
encPieces = headerPieces[1].split("|");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: // HmacSha256 types are deprecated
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
break;
|
||||
default:
|
||||
throw new Error("encType unavailable.");
|
||||
}
|
||||
|
||||
if (encPieces == null || encPieces.length <= 0) {
|
||||
throw new Error("encPieces unavailable.");
|
||||
}
|
||||
|
||||
const data = Utils.fromB64ToArray(encPieces[0]).buffer;
|
||||
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
|
||||
if (privateKey == null) {
|
||||
throw new Error("No private key.");
|
||||
}
|
||||
|
||||
let alg: "sha1" | "sha256" = "sha1";
|
||||
switch (encType) {
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
alg = "sha256";
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
break;
|
||||
default:
|
||||
throw new Error("encType unavailable.");
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg);
|
||||
}
|
||||
|
||||
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
const iv = Utils.fromB64ToArray(encString.iv).buffer;
|
||||
const data = Utils.fromB64ToArray(encString.data).buffer;
|
||||
const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null;
|
||||
const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key);
|
||||
if (decipher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decipher;
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
||||
return await this.aesDecryptToUtf8(
|
||||
encString.encryptionType,
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key,
|
||||
);
|
||||
}
|
||||
|
||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
if (encBuf == null) {
|
||||
throw new Error("no encBuf.");
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(encBuf);
|
||||
const encType = encBytes[0];
|
||||
let ctBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let ivBytes: Uint8Array<ArrayBuffer> = null;
|
||||
let macBytes: Uint8Array<ArrayBuffer> = null;
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if (encBytes.length <= 49) {
|
||||
// 1 + 16 + 32 + ctLength
|
||||
return null;
|
||||
}
|
||||
|
||||
ivBytes = encBytes.slice(1, 17);
|
||||
macBytes = encBytes.slice(17, 49);
|
||||
ctBytes = encBytes.slice(49);
|
||||
break;
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if (encBytes.length <= 17) {
|
||||
// 1 + 16 + ctLength
|
||||
return null;
|
||||
}
|
||||
|
||||
ivBytes = encBytes.slice(1, 17);
|
||||
ctBytes = encBytes.slice(17);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.aesDecryptToBytes(
|
||||
encType,
|
||||
ctBytes.buffer,
|
||||
ivBytes.buffer,
|
||||
macBytes != null ? macBytes.buffer : null,
|
||||
key,
|
||||
);
|
||||
}
|
||||
|
||||
// EFForg/OpenWireless
|
||||
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
|
||||
async randomNumber(min: number, max: number): Promise<number> {
|
||||
let rval = 0;
|
||||
const range = max - min + 1;
|
||||
const bitsNeeded = Math.ceil(Math.log2(range));
|
||||
if (bitsNeeded > 53) {
|
||||
throw new Error("We cannot generate numbers larger than 53 bits.");
|
||||
}
|
||||
|
||||
const bytesNeeded = Math.ceil(bitsNeeded / 8);
|
||||
const mask = Math.pow(2, bitsNeeded) - 1;
|
||||
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
|
||||
|
||||
// Fill a byte array with N random numbers
|
||||
const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded));
|
||||
|
||||
let p = (bytesNeeded - 1) * 8;
|
||||
for (let i = 0; i < bytesNeeded; i++) {
|
||||
rval += byteArray[i] * Math.pow(2, p);
|
||||
p -= 8;
|
||||
}
|
||||
|
||||
// Use & to apply the mask and reduce the number of recursive lookups
|
||||
rval = rval & mask;
|
||||
|
||||
if (rval >= range) {
|
||||
// Integer out of acceptable range
|
||||
return this.randomNumber(min, max);
|
||||
}
|
||||
|
||||
// Return an integer that falls within the range
|
||||
return min + rval;
|
||||
}
|
||||
|
||||
async validateKey(key: SymmetricCryptoKey) {
|
||||
try {
|
||||
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
|
||||
const encKey = await this.getEncKeyHelper(key);
|
||||
if (encPrivateKey == null || encKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
|
||||
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
} else if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) {
|
||||
let shouldStoreKey = false;
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId });
|
||||
shouldStoreKey = vaultTimeout == null;
|
||||
} else if (keySuffix === KeySuffixOptions.Biometric) {
|
||||
const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId });
|
||||
shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
||||
}
|
||||
return shouldStoreKey;
|
||||
}
|
||||
|
||||
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) {
|
||||
return keySuffix === KeySuffixOptions.Auto
|
||||
? await this.stateService.getCryptoMasterKeyAuto({ userId: userId })
|
||||
: await this.stateService.getCryptoMasterKeyBiometric({ userId: userId });
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = await this.getKeyForEncryption(key);
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
|
||||
|
||||
if (obj.key.macKey != null) {
|
||||
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
|
||||
macData.set(new Uint8Array(obj.iv), 0);
|
||||
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
|
||||
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private async aesDecryptToUtf8(
|
||||
encType: EncryptionType,
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<string> {
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encType, keyForEnc);
|
||||
|
||||
if (theKey.macKey != null && mac == null) {
|
||||
this.logService.error("mac required.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.encType !== encType) {
|
||||
this.logService.error("encType unavailable.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey);
|
||||
if (fastParams.macKey != null && fastParams.mac != null) {
|
||||
const computedMac = await this.cryptoFunctionService.hmacFast(
|
||||
fastParams.macData,
|
||||
fastParams.macKey,
|
||||
"sha256",
|
||||
);
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logService.error("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
||||
}
|
||||
|
||||
private async aesDecryptToBytes(
|
||||
encType: EncryptionType,
|
||||
data: ArrayBuffer,
|
||||
iv: ArrayBuffer,
|
||||
mac: ArrayBuffer,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<ArrayBuffer> {
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encType, keyForEnc);
|
||||
|
||||
if (theKey.macKey != null && mac == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.encType !== encType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.macKey != null && mac != null) {
|
||||
const macData = new Uint8Array(iv.byteLength + data.byteLength);
|
||||
macData.set(new Uint8Array(iv), 0);
|
||||
macData.set(new Uint8Array(data), iv.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(
|
||||
macData.buffer,
|
||||
theKey.macKey,
|
||||
"sha256",
|
||||
);
|
||||
if (computedMac === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logService.error("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey);
|
||||
}
|
||||
|
||||
private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
const encKey = await this.getEncKey();
|
||||
if (encKey != null) {
|
||||
return encKey;
|
||||
}
|
||||
|
||||
return await this.getKey();
|
||||
}
|
||||
|
||||
private async resolveLegacyKey(
|
||||
encType: EncryptionType,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
if (
|
||||
encType === EncryptionType.AesCbc128_HmacSha256_B64 &&
|
||||
key.encType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
// Old encrypt-then-mac scheme, make a new key
|
||||
let legacyKey = await this.stateService.getLegacyEtmKey();
|
||||
if (legacyKey == null) {
|
||||
legacyKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
await this.stateService.setLegacyEtmKey(legacyKey);
|
||||
}
|
||||
return legacyKey;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
const newKey = new Uint8Array(64);
|
||||
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
|
||||
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
|
||||
newKey.set(new Uint8Array(encKey));
|
||||
newKey.set(new Uint8Array(macKey), 32);
|
||||
return new SymmetricCryptoKey(newKey.buffer);
|
||||
}
|
||||
|
||||
private async hashPhrase(hash: ArrayBuffer, minimumEntropy = 64) {
|
||||
const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2);
|
||||
let numWords = Math.ceil(minimumEntropy / entropyPerWord);
|
||||
|
||||
const hashArr = Array.from(new Uint8Array(hash));
|
||||
const entropyAvailable = hashArr.length * 4;
|
||||
if (numWords * entropyPerWord > entropyAvailable) {
|
||||
throw new Error("Output entropy of hash function is too small");
|
||||
}
|
||||
|
||||
const phrase: string[] = [];
|
||||
let hashNumber = bigInt.fromArray(hashArr, 256);
|
||||
while (numWords--) {
|
||||
const remainder = hashNumber.mod(EEFLongWordList.length);
|
||||
hashNumber = hashNumber.divide(EEFLongWordList.length);
|
||||
phrase.push(EEFLongWordList[remainder as any]);
|
||||
}
|
||||
return phrase;
|
||||
}
|
||||
|
||||
private async buildEncKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: ArrayBuffer,
|
||||
): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
let encKeyEnc: EncString = null;
|
||||
if (key.key.byteLength === 32) {
|
||||
const newKey = await this.stretchKey(key);
|
||||
encKeyEnc = await this.encrypt(encKey, newKey);
|
||||
} else if (key.key.byteLength === 64) {
|
||||
encKeyEnc = await this.encrypt(encKey, key);
|
||||
} else {
|
||||
throw new Error("Invalid key size.");
|
||||
}
|
||||
return [new SymmetricCryptoKey(encKey), encKeyEnc];
|
||||
}
|
||||
|
||||
private async clearSecretKeyStore(userId?: string): Promise<void> {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
|
||||
private async getEncKeyHelper(key: SymmetricCryptoKey = null): Promise<SymmetricCryptoKey> {
|
||||
const inMemoryKey = await this.stateService.getDecryptedCryptoSymmetricKey();
|
||||
if (inMemoryKey != null) {
|
||||
return inMemoryKey;
|
||||
}
|
||||
|
||||
const encKey = await this.stateService.getEncryptedCryptoSymmetricKey();
|
||||
if (encKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key == null) {
|
||||
key = await this.getKey();
|
||||
}
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let decEncKey: ArrayBuffer;
|
||||
const encKeyCipher = new EncString(encKey);
|
||||
if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
decEncKey = await this.decryptToBytes(encKeyCipher, key);
|
||||
} else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const newKey = await this.stretchKey(key);
|
||||
decEncKey = await this.decryptToBytes(encKeyCipher, newKey);
|
||||
} else {
|
||||
throw new Error("Unsupported encKey type.");
|
||||
}
|
||||
if (decEncKey == null) {
|
||||
return null;
|
||||
}
|
||||
const symmetricCryptoKey = new SymmetricCryptoKey(decEncKey);
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(symmetricCryptoKey);
|
||||
return symmetricCryptoKey;
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { concatMap, distinctUntilChanged, Observable, Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Urls,
|
||||
} from "../abstractions/environment.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||
|
||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
private readonly urlsSubject = new Subject<Urls>();
|
||||
urls: Observable<Urls> = this.urlsSubject;
|
||||
|
||||
private baseUrl: string;
|
||||
private webVaultUrl: string;
|
||||
private apiUrl: string;
|
||||
private identityUrl: string;
|
||||
private iconsUrl: string;
|
||||
private notificationsUrl: string;
|
||||
private eventsUrl: string;
|
||||
private keyConnectorUrl: string;
|
||||
|
||||
constructor(private stateService: StateService) {
|
||||
this.stateService.activeAccount$
|
||||
.pipe(
|
||||
// Use == here to not trigger on undefined -> null transition
|
||||
distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId),
|
||||
concatMap(async () => {
|
||||
await this.setUrlsFromStorage();
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
hasBaseUrl() {
|
||||
return this.baseUrl != null;
|
||||
}
|
||||
|
||||
getNotificationsUrl() {
|
||||
if (this.notificationsUrl != null) {
|
||||
return this.notificationsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl != null) {
|
||||
return this.baseUrl + "/notifications";
|
||||
}
|
||||
|
||||
return "https://notifications.bitwarden.com";
|
||||
}
|
||||
|
||||
getWebVaultUrl() {
|
||||
if (this.webVaultUrl != null) {
|
||||
return this.webVaultUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl;
|
||||
}
|
||||
return "https://vault.bitwarden.com";
|
||||
}
|
||||
|
||||
getSendUrl() {
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://send.bitwarden.com/#"
|
||||
: this.getWebVaultUrl() + "/#/send/";
|
||||
}
|
||||
|
||||
getIconsUrl() {
|
||||
if (this.iconsUrl != null) {
|
||||
return this.iconsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/icons";
|
||||
}
|
||||
|
||||
return "https://icons.bitwarden.net";
|
||||
}
|
||||
|
||||
getApiUrl() {
|
||||
if (this.apiUrl != null) {
|
||||
return this.apiUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/api";
|
||||
}
|
||||
|
||||
return "https://api.bitwarden.com";
|
||||
}
|
||||
|
||||
getIdentityUrl() {
|
||||
if (this.identityUrl != null) {
|
||||
return this.identityUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/identity";
|
||||
}
|
||||
|
||||
return "https://identity.bitwarden.com";
|
||||
}
|
||||
|
||||
getEventsUrl() {
|
||||
if (this.eventsUrl != null) {
|
||||
return this.eventsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/events";
|
||||
}
|
||||
|
||||
return "https://events.bitwarden.com";
|
||||
}
|
||||
|
||||
getKeyConnectorUrl() {
|
||||
return this.keyConnectorUrl;
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||
const envUrls = new EnvironmentUrls();
|
||||
|
||||
this.baseUrl = envUrls.base = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = envUrls.api = urls.api;
|
||||
this.identityUrl = envUrls.identity = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = envUrls.events = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
}
|
||||
|
||||
async setUrls(urls: Urls): Promise<Urls> {
|
||||
urls.base = this.formatUrl(urls.base);
|
||||
urls.webVault = this.formatUrl(urls.webVault);
|
||||
urls.api = this.formatUrl(urls.api);
|
||||
urls.identity = this.formatUrl(urls.identity);
|
||||
urls.icons = this.formatUrl(urls.icons);
|
||||
urls.notifications = this.formatUrl(urls.notifications);
|
||||
urls.events = this.formatUrl(urls.events);
|
||||
urls.keyConnector = this.formatUrl(urls.keyConnector);
|
||||
|
||||
await this.stateService.setEnvironmentUrls({
|
||||
base: urls.base,
|
||||
api: urls.api,
|
||||
identity: urls.identity,
|
||||
webVault: urls.webVault,
|
||||
icons: urls.icons,
|
||||
notifications: urls.notifications,
|
||||
events: urls.events,
|
||||
keyConnector: urls.keyConnector,
|
||||
});
|
||||
|
||||
this.baseUrl = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = urls.api;
|
||||
this.identityUrl = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
|
||||
this.urlsSubject.next(urls);
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
getUrls() {
|
||||
return {
|
||||
base: this.baseUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
icons: this.iconsUrl,
|
||||
notifications: this.notificationsUrl,
|
||||
events: this.eventsUrl,
|
||||
keyConnector: this.keyConnectorUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private formatUrl(url: string): string {
|
||||
if (url == null || url === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = url.replace(/\/+$/g, "");
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
return url.trim();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,504 +0,0 @@
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { StateVersion } from "../enums/stateVersion";
|
||||
import { ThemeType } from "../enums/themeType";
|
||||
import { StateFactory } from "../factories/stateFactory";
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { ProviderData } from "../models/data/providerData";
|
||||
import { Account, AccountSettings } from "../models/domain/account";
|
||||
import { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||
import { GlobalState } from "../models/domain/globalState";
|
||||
import { StorageOptions } from "../models/domain/storageOptions";
|
||||
|
||||
import { TokenService } from "./token.service";
|
||||
|
||||
// Originally (before January 2022) storage was handled as a flat key/value pair store.
|
||||
// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration.
|
||||
const v1Keys: { [key: string]: string } = {
|
||||
accessToken: "accessToken",
|
||||
alwaysShowDock: "alwaysShowDock",
|
||||
autoConfirmFingerprints: "autoConfirmFingerprints",
|
||||
autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault",
|
||||
biometricAwaitingAcceptance: "biometricAwaitingAcceptance",
|
||||
biometricFingerprintValidated: "biometricFingerprintValidated",
|
||||
biometricText: "biometricText",
|
||||
biometricUnlock: "biometric",
|
||||
clearClipboard: "clearClipboardKey",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
collapsedGroupings: "collapsedGroupings",
|
||||
convertAccountToKeyConnector: "convertAccountToKeyConnector",
|
||||
defaultUriMatch: "defaultUriMatch",
|
||||
disableAddLoginNotification: "disableAddLoginNotification",
|
||||
disableAutoBiometricsPrompt: "noAutoPromptBiometrics",
|
||||
disableAutoTotpCopy: "disableAutoTotpCopy",
|
||||
disableBadgeCounter: "disableBadgeCounter",
|
||||
disableChangedPasswordNotification: "disableChangedPasswordNotification",
|
||||
disableContextMenuItem: "disableContextMenuItem",
|
||||
disableFavicon: "disableFavicon",
|
||||
disableGa: "disableGa",
|
||||
dontShowCardsCurrentTab: "dontShowCardsCurrentTab",
|
||||
dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab",
|
||||
emailVerified: "emailVerified",
|
||||
enableAlwaysOnTop: "enableAlwaysOnTopKey",
|
||||
enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad",
|
||||
enableBiometric: "enabledBiometric",
|
||||
enableBrowserIntegration: "enableBrowserIntegration",
|
||||
enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint",
|
||||
enableCloseToTray: "enableCloseToTray",
|
||||
enableFullWidth: "enableFullWidth",
|
||||
enableGravatars: "enableGravatars",
|
||||
enableMinimizeToTray: "enableMinimizeToTray",
|
||||
enableStartToTray: "enableStartToTrayKey",
|
||||
enableTray: "enableTray",
|
||||
encKey: "encKey", // Generated Symmetric Key
|
||||
encOrgKeys: "encOrgKeys",
|
||||
encPrivate: "encPrivateKey",
|
||||
encProviderKeys: "encProviderKeys",
|
||||
entityId: "entityId",
|
||||
entityType: "entityType",
|
||||
environmentUrls: "environmentUrls",
|
||||
equivalentDomains: "equivalentDomains",
|
||||
eventCollection: "eventCollection",
|
||||
forcePasswordReset: "forcePasswordReset",
|
||||
history: "generatedPasswordHistory",
|
||||
installedVersion: "installedVersion",
|
||||
kdf: "kdf",
|
||||
kdfIterations: "kdfIterations",
|
||||
key: "key", // Master Key
|
||||
keyHash: "keyHash",
|
||||
lastActive: "lastActive",
|
||||
localData: "sitesLocalData",
|
||||
locale: "locale",
|
||||
mainWindowSize: "mainWindowSize",
|
||||
minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey",
|
||||
neverDomains: "neverDomains",
|
||||
noAutoPromptBiometricsText: "noAutoPromptBiometricsText",
|
||||
openAtLogin: "openAtLogin",
|
||||
passwordGenerationOptions: "passwordGenerationOptions",
|
||||
pinProtected: "pinProtectedKey",
|
||||
protectedPin: "protectedPin",
|
||||
refreshToken: "refreshToken",
|
||||
ssoCodeVerifier: "ssoCodeVerifier",
|
||||
ssoIdentifier: "ssoOrgIdentifier",
|
||||
ssoState: "ssoState",
|
||||
stamp: "securityStamp",
|
||||
theme: "theme",
|
||||
userEmail: "userEmail",
|
||||
userId: "userId",
|
||||
usesConnector: "usesKeyConnector",
|
||||
vaultTimeoutAction: "vaultTimeoutAction",
|
||||
vaultTimeout: "lockOption",
|
||||
rememberedEmail: "rememberedEmail",
|
||||
};
|
||||
|
||||
const v1KeyPrefixes: { [key: string]: string } = {
|
||||
ciphers: "ciphers_",
|
||||
collections: "collections_",
|
||||
folders: "folders_",
|
||||
lastSync: "lastSync_",
|
||||
policies: "policies_",
|
||||
twoFactorToken: "twoFactorToken_",
|
||||
organizations: "organizations_",
|
||||
providers: "providers_",
|
||||
sends: "sends_",
|
||||
settings: "settings_",
|
||||
};
|
||||
|
||||
const keys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
activeUserId: "activeUserId",
|
||||
tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication
|
||||
accountActivity: "accountActivity",
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
autoKey: "_masterkey_auto",
|
||||
biometricKey: "_masterkey_biometric",
|
||||
masterKey: "_masterkey",
|
||||
};
|
||||
|
||||
export class StateMigrationService<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account,
|
||||
> {
|
||||
constructor(
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
||||
) {}
|
||||
|
||||
async needsMigration(): Promise<boolean> {
|
||||
const currentStateVersion = await this.getCurrentStateVersion();
|
||||
return currentStateVersion == null || currentStateVersion < StateVersion.Latest;
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion = await this.getCurrentStateVersion();
|
||||
while (currentStateVersion < StateVersion.Latest) {
|
||||
switch (currentStateVersion) {
|
||||
case StateVersion.One:
|
||||
await this.migrateStateFrom1To2();
|
||||
break;
|
||||
case StateVersion.Two:
|
||||
await this.migrateStateFrom2To3();
|
||||
break;
|
||||
case StateVersion.Three:
|
||||
await this.migrateStateFrom3To4();
|
||||
break;
|
||||
}
|
||||
|
||||
currentStateVersion += 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrateStateFrom1To2(): Promise<void> {
|
||||
const clearV1Keys = async (clearingUserId?: string) => {
|
||||
for (const key in v1Keys) {
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
await this.set(v1Keys[key], null);
|
||||
}
|
||||
if (clearingUserId != null) {
|
||||
for (const keyPrefix in v1KeyPrefixes) {
|
||||
if (keyPrefix == null) {
|
||||
continue;
|
||||
}
|
||||
await this.set(v1KeyPrefixes[keyPrefix] + userId, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Some processes, like biometrics, may have already defined a value before migrations are run.
|
||||
// We don't want to null out those values if they don't exist in the old storage scheme (like for new installs)
|
||||
// So, the OOO for migration is that we:
|
||||
// 1. Check for an existing storage value from the old storage structure OR
|
||||
// 2. Check for a value already set by processes that run before migration OR
|
||||
// 3. Assign the default value
|
||||
const globals =
|
||||
(await this.get<GlobalState>(keys.global)) ?? this.stateFactory.createGlobal(null);
|
||||
globals.stateVersion = StateVersion.Two;
|
||||
globals.environmentUrls =
|
||||
(await this.get<EnvironmentUrls>(v1Keys.environmentUrls)) ?? globals.environmentUrls;
|
||||
globals.locale = (await this.get<string>(v1Keys.locale)) ?? globals.locale;
|
||||
globals.noAutoPromptBiometrics =
|
||||
(await this.get<boolean>(v1Keys.disableAutoBiometricsPrompt)) ??
|
||||
globals.noAutoPromptBiometrics;
|
||||
globals.noAutoPromptBiometricsText =
|
||||
(await this.get<string>(v1Keys.noAutoPromptBiometricsText)) ??
|
||||
globals.noAutoPromptBiometricsText;
|
||||
globals.ssoCodeVerifier =
|
||||
(await this.get<string>(v1Keys.ssoCodeVerifier)) ?? globals.ssoCodeVerifier;
|
||||
globals.ssoOrganizationIdentifier =
|
||||
(await this.get<string>(v1Keys.ssoIdentifier)) ?? globals.ssoOrganizationIdentifier;
|
||||
globals.ssoState = (await this.get<any>(v1Keys.ssoState)) ?? globals.ssoState;
|
||||
globals.rememberedEmail =
|
||||
(await this.get<string>(v1Keys.rememberedEmail)) ?? globals.rememberedEmail;
|
||||
globals.theme = (await this.get<ThemeType>(v1Keys.theme)) ?? globals.theme;
|
||||
globals.vaultTimeout = (await this.get<number>(v1Keys.vaultTimeout)) ?? globals.vaultTimeout;
|
||||
globals.vaultTimeoutAction =
|
||||
(await this.get<string>(v1Keys.vaultTimeoutAction)) ?? globals.vaultTimeoutAction;
|
||||
globals.window = (await this.get<any>(v1Keys.mainWindowSize)) ?? globals.window;
|
||||
globals.enableTray = (await this.get<boolean>(v1Keys.enableTray)) ?? globals.enableTray;
|
||||
globals.enableMinimizeToTray =
|
||||
(await this.get<boolean>(v1Keys.enableMinimizeToTray)) ?? globals.enableMinimizeToTray;
|
||||
globals.enableCloseToTray =
|
||||
(await this.get<boolean>(v1Keys.enableCloseToTray)) ?? globals.enableCloseToTray;
|
||||
globals.enableStartToTray =
|
||||
(await this.get<boolean>(v1Keys.enableStartToTray)) ?? globals.enableStartToTray;
|
||||
globals.openAtLogin = (await this.get<boolean>(v1Keys.openAtLogin)) ?? globals.openAtLogin;
|
||||
globals.alwaysShowDock =
|
||||
(await this.get<boolean>(v1Keys.alwaysShowDock)) ?? globals.alwaysShowDock;
|
||||
globals.enableBrowserIntegration =
|
||||
(await this.get<boolean>(v1Keys.enableBrowserIntegration)) ??
|
||||
globals.enableBrowserIntegration;
|
||||
globals.enableBrowserIntegrationFingerprint =
|
||||
(await this.get<boolean>(v1Keys.enableBrowserIntegrationFingerprint)) ??
|
||||
globals.enableBrowserIntegrationFingerprint;
|
||||
|
||||
const userId =
|
||||
(await this.get<string>(v1Keys.userId)) ?? (await this.get<string>(v1Keys.entityId));
|
||||
|
||||
const defaultAccount = this.stateFactory.createAccount(null);
|
||||
const accountSettings: AccountSettings = {
|
||||
autoConfirmFingerPrints:
|
||||
(await this.get<boolean>(v1Keys.autoConfirmFingerprints)) ??
|
||||
defaultAccount.settings.autoConfirmFingerPrints,
|
||||
autoFillOnPageLoadDefault:
|
||||
(await this.get<boolean>(v1Keys.autoFillOnPageLoadDefault)) ??
|
||||
defaultAccount.settings.autoFillOnPageLoadDefault,
|
||||
biometricLocked: null,
|
||||
biometricUnlock:
|
||||
(await this.get<boolean>(v1Keys.biometricUnlock)) ??
|
||||
defaultAccount.settings.biometricUnlock,
|
||||
clearClipboard:
|
||||
(await this.get<number>(v1Keys.clearClipboard)) ?? defaultAccount.settings.clearClipboard,
|
||||
defaultUriMatch:
|
||||
(await this.get<any>(v1Keys.defaultUriMatch)) ?? defaultAccount.settings.defaultUriMatch,
|
||||
disableAddLoginNotification:
|
||||
(await this.get<boolean>(v1Keys.disableAddLoginNotification)) ??
|
||||
defaultAccount.settings.disableAddLoginNotification,
|
||||
disableAutoBiometricsPrompt:
|
||||
(await this.get<boolean>(v1Keys.disableAutoBiometricsPrompt)) ??
|
||||
defaultAccount.settings.disableAutoBiometricsPrompt,
|
||||
disableAutoTotpCopy:
|
||||
(await this.get<boolean>(v1Keys.disableAutoTotpCopy)) ??
|
||||
defaultAccount.settings.disableAutoTotpCopy,
|
||||
disableBadgeCounter:
|
||||
(await this.get<boolean>(v1Keys.disableBadgeCounter)) ??
|
||||
defaultAccount.settings.disableBadgeCounter,
|
||||
disableChangedPasswordNotification:
|
||||
(await this.get<boolean>(v1Keys.disableChangedPasswordNotification)) ??
|
||||
defaultAccount.settings.disableChangedPasswordNotification,
|
||||
disableContextMenuItem:
|
||||
(await this.get<boolean>(v1Keys.disableContextMenuItem)) ??
|
||||
defaultAccount.settings.disableContextMenuItem,
|
||||
disableGa: (await this.get<boolean>(v1Keys.disableGa)) ?? defaultAccount.settings.disableGa,
|
||||
dontShowCardsCurrentTab:
|
||||
(await this.get<boolean>(v1Keys.dontShowCardsCurrentTab)) ??
|
||||
defaultAccount.settings.dontShowCardsCurrentTab,
|
||||
dontShowIdentitiesCurrentTab:
|
||||
(await this.get<boolean>(v1Keys.dontShowIdentitiesCurrentTab)) ??
|
||||
defaultAccount.settings.dontShowIdentitiesCurrentTab,
|
||||
enableAlwaysOnTop:
|
||||
(await this.get<boolean>(v1Keys.enableAlwaysOnTop)) ??
|
||||
defaultAccount.settings.enableAlwaysOnTop,
|
||||
enableAutoFillOnPageLoad:
|
||||
(await this.get<boolean>(v1Keys.enableAutoFillOnPageLoad)) ??
|
||||
defaultAccount.settings.enableAutoFillOnPageLoad,
|
||||
enableBiometric:
|
||||
(await this.get<boolean>(v1Keys.enableBiometric)) ??
|
||||
defaultAccount.settings.enableBiometric,
|
||||
enableFullWidth:
|
||||
(await this.get<boolean>(v1Keys.enableFullWidth)) ??
|
||||
defaultAccount.settings.enableFullWidth,
|
||||
enableGravitars:
|
||||
(await this.get<boolean>(v1Keys.enableGravatars)) ??
|
||||
defaultAccount.settings.enableGravitars,
|
||||
environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls,
|
||||
equivalentDomains:
|
||||
(await this.get<any>(v1Keys.equivalentDomains)) ??
|
||||
defaultAccount.settings.equivalentDomains,
|
||||
minimizeOnCopyToClipboard:
|
||||
(await this.get<boolean>(v1Keys.minimizeOnCopyToClipboard)) ??
|
||||
defaultAccount.settings.minimizeOnCopyToClipboard,
|
||||
neverDomains:
|
||||
(await this.get<any>(v1Keys.neverDomains)) ?? defaultAccount.settings.neverDomains,
|
||||
passwordGenerationOptions:
|
||||
(await this.get<any>(v1Keys.passwordGenerationOptions)) ??
|
||||
defaultAccount.settings.passwordGenerationOptions,
|
||||
pinProtected: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<string>(v1Keys.pinProtected),
|
||||
},
|
||||
protectedPin: await this.get<string>(v1Keys.protectedPin),
|
||||
settings: userId == null ? null : await this.get<any>(v1KeyPrefixes.settings + userId),
|
||||
vaultTimeout:
|
||||
(await this.get<number>(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout,
|
||||
vaultTimeoutAction:
|
||||
(await this.get<string>(v1Keys.vaultTimeoutAction)) ??
|
||||
defaultAccount.settings.vaultTimeoutAction,
|
||||
};
|
||||
|
||||
// (userId == null) = no logged in user (so no known userId) and we need to temporarily store account specific settings in state to migrate on first auth
|
||||
// (userId != null) = we have a currently authed user (so known userId) with encrypted data and other key settings we can move, no need to temporarily store account settings
|
||||
if (userId == null) {
|
||||
await this.set(keys.tempAccountSettings, accountSettings);
|
||||
await this.set(keys.global, globals);
|
||||
await this.set(keys.authenticatedAccounts, []);
|
||||
await this.set(keys.activeUserId, null);
|
||||
await clearV1Keys();
|
||||
return;
|
||||
}
|
||||
|
||||
globals.twoFactorToken = await this.get<string>(v1KeyPrefixes.twoFactorToken + userId);
|
||||
await this.set(keys.global, globals);
|
||||
await this.set(userId, {
|
||||
data: {
|
||||
addEditCipherInfo: null,
|
||||
ciphers: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.ciphers + userId),
|
||||
},
|
||||
collapsedGroupings: null,
|
||||
collections: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.collections + userId),
|
||||
},
|
||||
eventCollection: await this.get<any[]>(v1Keys.eventCollection),
|
||||
folders: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.folders + userId),
|
||||
},
|
||||
localData: null,
|
||||
organizations: await this.get<{ [id: string]: OrganizationData }>(
|
||||
v1KeyPrefixes.organizations + userId,
|
||||
),
|
||||
passwordGenerationHistory: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any[]>(v1Keys.history),
|
||||
},
|
||||
policies: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.policies + userId),
|
||||
},
|
||||
providers: await this.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId),
|
||||
sends: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: any }>(v1KeyPrefixes.sends + userId),
|
||||
},
|
||||
},
|
||||
keys: {
|
||||
apiKeyClientSecret: await this.get<string>(v1Keys.clientSecret),
|
||||
cryptoMasterKey: null,
|
||||
cryptoMasterKeyAuto: null,
|
||||
cryptoMasterKeyB64: null,
|
||||
cryptoMasterKeyBiometric: null,
|
||||
cryptoSymmetricKey: {
|
||||
encrypted: await this.get<string>(v1Keys.encKey),
|
||||
decrypted: null,
|
||||
},
|
||||
legacyEtmKey: null,
|
||||
organizationKeys: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any>(v1Keys.encOrgKeys),
|
||||
},
|
||||
privateKey: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<string>(v1Keys.encPrivate),
|
||||
},
|
||||
providerKeys: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any>(v1Keys.encProviderKeys),
|
||||
},
|
||||
publicKey: null,
|
||||
},
|
||||
profile: {
|
||||
apiKeyClientId: await this.get<string>(v1Keys.clientId),
|
||||
authenticationStatus: null,
|
||||
convertAccountToKeyConnector: await this.get<boolean>(v1Keys.convertAccountToKeyConnector),
|
||||
email: await this.get<string>(v1Keys.userEmail),
|
||||
emailVerified: await this.get<boolean>(v1Keys.emailVerified),
|
||||
entityId: null,
|
||||
entityType: null,
|
||||
everBeenUnlocked: null,
|
||||
forcePasswordReset: null,
|
||||
hasPremiumPersonally: null,
|
||||
kdfIterations: await this.get<number>(v1Keys.kdfIterations),
|
||||
kdfType: await this.get<KdfType>(v1Keys.kdf),
|
||||
keyHash: await this.get<string>(v1Keys.keyHash),
|
||||
lastSync: null,
|
||||
userId: userId,
|
||||
usesKeyConnector: null,
|
||||
},
|
||||
settings: accountSettings,
|
||||
tokens: {
|
||||
accessToken: await this.get<string>(v1Keys.accessToken),
|
||||
decodedToken: null,
|
||||
refreshToken: await this.get<string>(v1Keys.refreshToken),
|
||||
securityStamp: null,
|
||||
},
|
||||
});
|
||||
|
||||
await this.set(keys.authenticatedAccounts, [userId]);
|
||||
await this.set(keys.activeUserId, userId);
|
||||
|
||||
const accountActivity: { [userId: string]: number } = {
|
||||
[userId]: await this.get<number>(v1Keys.lastActive),
|
||||
};
|
||||
accountActivity[userId] = await this.get<number>(v1Keys.lastActive);
|
||||
await this.set(keys.accountActivity, accountActivity);
|
||||
|
||||
await clearV1Keys(userId);
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}${partialKeys.biometricKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }),
|
||||
{ keySuffix: "biometric" },
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" });
|
||||
}
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}${partialKeys.autoKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }),
|
||||
{ keySuffix: "auto" },
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" });
|
||||
}
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key)) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}${partialKeys.masterKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key),
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key);
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrateStateFrom2To3(): Promise<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<TAccount>(userId);
|
||||
if (
|
||||
account?.profile?.hasPremiumPersonally === null &&
|
||||
account.tokens?.accessToken != null
|
||||
) {
|
||||
const decodedToken = await TokenService.decodeToken(account.tokens.accessToken);
|
||||
account.profile.hasPremiumPersonally = decodedToken.premium;
|
||||
await this.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Three;
|
||||
await this.set(keys.global, globals);
|
||||
}
|
||||
|
||||
protected async migrateStateFrom3To4(): Promise<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<TAccount>(userId);
|
||||
if (account?.profile?.everBeenUnlocked != null) {
|
||||
delete account.profile.everBeenUnlocked;
|
||||
return this.set(userId, account);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Four;
|
||||
await this.set(keys.global, globals);
|
||||
}
|
||||
|
||||
protected get options(): StorageOptions {
|
||||
return { htmlStorageLocation: HtmlStorageLocation.Local };
|
||||
}
|
||||
|
||||
protected get<T>(key: string): Promise<T> {
|
||||
return this.storageService.get<T>(key, this.options);
|
||||
}
|
||||
|
||||
protected set(key: string, value: any): Promise<any> {
|
||||
if (value == null) {
|
||||
return this.storageService.remove(key, this.options);
|
||||
}
|
||||
return this.storageService.save(key, value, this.options);
|
||||
}
|
||||
|
||||
protected async getGlobals(): Promise<TGlobalState> {
|
||||
return await this.get<TGlobalState>(keys.global);
|
||||
}
|
||||
|
||||
protected async getCurrentStateVersion(): Promise<StateVersion> {
|
||||
return (await this.getGlobals())?.stateVersion ?? StateVersion.One;
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export class TokenService implements TokenServiceAbstraction {
|
||||
static decodeToken(token: string): Promise<any> {
|
||||
if (token == null) {
|
||||
throw new Error("Token not provided.");
|
||||
}
|
||||
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("JWT must have 3 parts");
|
||||
}
|
||||
|
||||
const decoded = Utils.fromUrlB64ToUtf8(parts[1]);
|
||||
if (decoded == null) {
|
||||
throw new Error("Cannot decode the token");
|
||||
}
|
||||
|
||||
const decodedToken = JSON.parse(decoded);
|
||||
return decodedToken;
|
||||
}
|
||||
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async setTokens(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret: [string, string],
|
||||
): Promise<any> {
|
||||
await this.setToken(accessToken);
|
||||
await this.setRefreshToken(refreshToken);
|
||||
if (clientIdClientSecret != null) {
|
||||
await this.setClientId(clientIdClientSecret[0]);
|
||||
await this.setClientSecret(clientIdClientSecret[1]);
|
||||
}
|
||||
}
|
||||
|
||||
async setClientId(clientId: string): Promise<any> {
|
||||
return await this.stateService.setApiKeyClientId(clientId);
|
||||
}
|
||||
|
||||
async getClientId(): Promise<string> {
|
||||
return await this.stateService.getApiKeyClientId();
|
||||
}
|
||||
|
||||
async setClientSecret(clientSecret: string): Promise<any> {
|
||||
return await this.stateService.setApiKeyClientSecret(clientSecret);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string> {
|
||||
return await this.stateService.getApiKeyClientSecret();
|
||||
}
|
||||
|
||||
async setToken(token: string): Promise<void> {
|
||||
await this.stateService.setAccessToken(token);
|
||||
}
|
||||
|
||||
async getToken(): Promise<string> {
|
||||
return await this.stateService.getAccessToken();
|
||||
}
|
||||
|
||||
async setRefreshToken(refreshToken: string): Promise<any> {
|
||||
return await this.stateService.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string> {
|
||||
return await this.stateService.getRefreshToken();
|
||||
}
|
||||
|
||||
async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string> {
|
||||
return await this.stateService.getTwoFactorToken();
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(null);
|
||||
}
|
||||
|
||||
async clearToken(userId?: string): Promise<any> {
|
||||
await this.stateService.setAccessToken(null, { userId: userId });
|
||||
await this.stateService.setRefreshToken(null, { userId: userId });
|
||||
await this.stateService.setApiKeyClientId(null, { userId: userId });
|
||||
await this.stateService.setApiKeyClientSecret(null, { userId: userId });
|
||||
}
|
||||
|
||||
// jwthelper methods
|
||||
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
|
||||
|
||||
async decodeToken(token?: string): Promise<any> {
|
||||
const storedToken = await this.stateService.getDecodedToken();
|
||||
if (token === null && storedToken != null) {
|
||||
return storedToken;
|
||||
}
|
||||
|
||||
token = token ?? (await this.stateService.getAccessToken());
|
||||
|
||||
if (token == null) {
|
||||
throw new Error("Token not found.");
|
||||
}
|
||||
|
||||
return TokenService.decodeToken(token);
|
||||
}
|
||||
|
||||
async getTokenExpirationDate(): Promise<Date> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.exp === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const d = new Date(0); // The 0 here is the key, which sets the date to the epoch
|
||||
d.setUTCSeconds(decoded.exp);
|
||||
return d;
|
||||
}
|
||||
|
||||
async tokenSecondsRemaining(offsetSeconds = 0): Promise<number> {
|
||||
const d = await this.getTokenExpirationDate();
|
||||
if (d == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const msRemaining = d.valueOf() - (new Date().valueOf() + offsetSeconds * 1000);
|
||||
return Math.round(msRemaining / 1000);
|
||||
}
|
||||
|
||||
async tokenNeedsRefresh(minutes = 5): Promise<boolean> {
|
||||
const sRemaining = await this.tokenSecondsRemaining();
|
||||
return sRemaining < 60 * minutes;
|
||||
}
|
||||
|
||||
async getUserId(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.sub === "undefined") {
|
||||
throw new Error("No user id found");
|
||||
}
|
||||
|
||||
return decoded.sub as string;
|
||||
}
|
||||
|
||||
async getEmail(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.email === "undefined") {
|
||||
throw new Error("No email found");
|
||||
}
|
||||
|
||||
return decoded.email as string;
|
||||
}
|
||||
|
||||
async getEmailVerified(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.email_verified === "undefined") {
|
||||
throw new Error("No email verification found");
|
||||
}
|
||||
|
||||
return decoded.email_verified as boolean;
|
||||
}
|
||||
|
||||
async getName(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.name === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decoded.name as string;
|
||||
}
|
||||
|
||||
async getPremium(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.premium === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return decoded.premium as boolean;
|
||||
}
|
||||
|
||||
async getIssuer(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.iss === "undefined") {
|
||||
throw new Error("No issuer found");
|
||||
}
|
||||
|
||||
return decoded.iss as string;
|
||||
}
|
||||
|
||||
async getIsExternal(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
|
||||
return Array.isArray(decoded.amr) && decoded.amr.includes("external");
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
}
|
||||
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
return await this.stateService.getEnableBiometric();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
@@ -203,12 +203,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
}
|
||||
|
||||
async getEffectiveTheme() {
|
||||
const theme = await this.stateService.getTheme();
|
||||
if (theme == null || theme === ThemeType.System) {
|
||||
return this.getDefaultSystemTheme();
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
return this.getDefaultSystemTheme();
|
||||
}
|
||||
|
||||
supportsSecureStorage(): boolean {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import Store from "electron-store";
|
||||
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
|
||||
|
||||
// eslint-disable-next-line
|
||||
const Store = require("electron-store");
|
||||
|
||||
export class ElectronStorageService implements StorageService {
|
||||
private store: any;
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
} from "electron";
|
||||
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
|
||||
import { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import * as url from "url";
|
||||
import { app, BrowserWindow, Rectangle, screen } from "electron";
|
||||
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
||||
|
||||
import { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
import { cleanUserAgent, isDev, isMacAppStore, isSnapStore } from "./utils";
|
||||
|
||||
@@ -244,13 +245,15 @@ export class WindowMain {
|
||||
}
|
||||
|
||||
private async getWindowState(defaultWidth: number, defaultHeight: number) {
|
||||
const state = await this.stateService.getWindow();
|
||||
let state = await this.stateService.getWindow();
|
||||
|
||||
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
|
||||
let displayBounds: Rectangle = null;
|
||||
if (!isValid) {
|
||||
state.width = defaultWidth;
|
||||
state.height = defaultHeight;
|
||||
state = {
|
||||
width: defaultWidth,
|
||||
height: defaultHeight,
|
||||
};
|
||||
|
||||
displayBounds = screen.getPrimaryDisplay().bounds;
|
||||
} else if (this.stateHasBounds(state) && state.displayBounds) {
|
||||
|
||||
@@ -99,8 +99,11 @@ export abstract class BaseProgram {
|
||||
protected async exitIfAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (authed) {
|
||||
const email = await this.stateService.getEmail();
|
||||
this.processResponse(Response.error("You are already logged in as " + email + "."), true);
|
||||
const organizationId = await this.stateService.getEntityId();
|
||||
this.processResponse(
|
||||
Response.error("You are already logged in to" + organizationId + "."),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
package.json
14
package.json
@@ -31,14 +31,14 @@
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"build:main": "webpack --config webpack.main.cjs",
|
||||
"build:renderer": "webpack --config webpack.renderer.cjs",
|
||||
"build:renderer:watch": "webpack --config webpack.renderer.cjs --watch",
|
||||
"build:main": "webpack --config webpack.main.mjs",
|
||||
"build:renderer": "webpack --config webpack.renderer.mjs",
|
||||
"build:renderer:watch": "webpack --config webpack.renderer.mjs --watch",
|
||||
"build:dist": "npm run reset && npm run rebuild && npm run build",
|
||||
"build:cli": "webpack --config webpack.cli.cjs",
|
||||
"build:cli:watch": "webpack --config webpack.cli.cjs --watch",
|
||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs",
|
||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs --watch",
|
||||
"build:cli": "webpack --config webpack.cli.mjs",
|
||||
"build:cli:watch": "webpack --config webpack.cli.mjs --watch",
|
||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.mjs",
|
||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.mjs --watch",
|
||||
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
|
||||
"electron:ignore": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 --ignore-certificate-errors ./build --watch\" \"npm run build:renderer:watch\"",
|
||||
"clean:dist": "rimraf --glob ./dist/*",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
require("dotenv").config();
|
||||
const { notarize } = require("@electron/notarize");
|
||||
import "dotenv/config";
|
||||
import notarizeModule from "@electron/notarize";
|
||||
|
||||
exports.default = async function notarizing(context) {
|
||||
const { notarize } = notarizeModule;
|
||||
|
||||
export default async function notarizing(context) {
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
if (electronPlatformName !== "darwin") {
|
||||
return;
|
||||
@@ -33,4 +34,4 @@ exports.default = async function notarizing(context) {
|
||||
appleIdPassword: appleIdPassword,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||
exports.default = async function (configuration) {
|
||||
import { execSync } from "child_process";
|
||||
|
||||
export default async function (configuration) {
|
||||
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
|
||||
console.log(`[*] Signing file: ${configuration.path}`);
|
||||
require("child_process").execSync(
|
||||
execSync(
|
||||
`azuresigntool sign ` +
|
||||
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
|
||||
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
|
||||
@@ -18,4 +19,4 @@ exports.default = async function (configuration) {
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
13
src/abstractions/environment.service.ts
Normal file
13
src/abstractions/environment.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
|
||||
export { EnvironmentUrls };
|
||||
|
||||
export abstract class EnvironmentService {
|
||||
abstract setUrls(urls: EnvironmentUrls): Promise<void>;
|
||||
abstract setUrlsFromStorage(): Promise<void>;
|
||||
|
||||
abstract hasBaseUrl(): boolean;
|
||||
abstract getApiUrl(): string;
|
||||
abstract getIdentityUrl(): string;
|
||||
abstract getWebVaultUrl(): string;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { StateService as BaseStateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
import { Account } from "@/src/models/account";
|
||||
import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
|
||||
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "@/src/models/ldapConfiguration";
|
||||
@@ -10,9 +9,9 @@ import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
export abstract class StateService extends BaseStateServiceAbstraction<Account> {
|
||||
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
|
||||
setDirectory: (
|
||||
export abstract class StateService {
|
||||
abstract getDirectory<IConfiguration>(type: DirectoryType): Promise<IConfiguration>;
|
||||
abstract setDirectory(
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
@@ -20,37 +19,89 @@ export abstract class StateService extends BaseStateServiceAbstraction<Account>
|
||||
| EntraIdConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration,
|
||||
) => Promise<any>;
|
||||
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
|
||||
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
|
||||
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getEntraConfiguration: (options?: StorageOptions) => Promise<EntraIdConfiguration>;
|
||||
setEntraConfiguration: (value: EntraIdConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
|
||||
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
|
||||
setOneLoginConfiguration: (
|
||||
): Promise<any>;
|
||||
abstract getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration>;
|
||||
abstract setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration>;
|
||||
abstract setGsuiteConfiguration(
|
||||
value: GSuiteConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration>;
|
||||
abstract setEntraConfiguration(
|
||||
value: EntraIdConfiguration,
|
||||
options?: StorageOptions,
|
||||
): Promise<void>;
|
||||
abstract getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration>;
|
||||
abstract setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration>;
|
||||
abstract setOneLoginConfiguration(
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getOrganizationId: (options?: StorageOptions) => Promise<string>;
|
||||
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
|
||||
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
|
||||
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
|
||||
getUserDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getGroupDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
|
||||
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
|
||||
): Promise<void>;
|
||||
abstract getOrganizationId(options?: StorageOptions): Promise<string>;
|
||||
abstract setOrganizationId(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getSync(options?: StorageOptions): Promise<SyncConfiguration>;
|
||||
abstract setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void>;
|
||||
abstract getDirectoryType(options?: StorageOptions): Promise<DirectoryType>;
|
||||
abstract setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void>;
|
||||
abstract getUserDelta(options?: StorageOptions): Promise<string>;
|
||||
abstract setUserDelta(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastUserSync(options?: StorageOptions): Promise<Date>;
|
||||
abstract setLastUserSync(value: Date, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastGroupSync(options?: StorageOptions): Promise<Date>;
|
||||
abstract setLastGroupSync(value: Date, options?: StorageOptions): Promise<void>;
|
||||
abstract getGroupDelta(options?: StorageOptions): Promise<string>;
|
||||
abstract setGroupDelta(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getLastSyncHash(options?: StorageOptions): Promise<string>;
|
||||
abstract setLastSyncHash(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getSyncingDir(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setSyncingDir(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract clearSyncSettings(syncHashToo: boolean): Promise<void>;
|
||||
|
||||
// Window settings (for WindowMain)
|
||||
abstract getWindow(options?: StorageOptions): Promise<any>;
|
||||
abstract setWindow(value: any, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableAlwaysOnTop(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Tray settings (for TrayMain)
|
||||
abstract getEnableTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableMinimizeToTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getEnableCloseToTray(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
abstract getAlwaysShowDock(options?: StorageOptions): Promise<boolean>;
|
||||
abstract setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Environment URLs (adding convenience methods)
|
||||
abstract getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls>;
|
||||
abstract setEnvironmentUrls(value: EnvironmentUrls): Promise<void>;
|
||||
abstract getApiUrl(options?: StorageOptions): Promise<string>;
|
||||
abstract getIdentityUrl(options?: StorageOptions): Promise<string>;
|
||||
|
||||
// Token management (replaces TokenService.clearToken())
|
||||
abstract clearAuthTokens(): Promise<void>;
|
||||
abstract getAccessToken(options?: StorageOptions): Promise<string>;
|
||||
abstract setAccessToken(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getRefreshToken(options?: StorageOptions): Promise<string>;
|
||||
abstract setRefreshToken(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getApiKeyClientId(options?: StorageOptions): Promise<string>;
|
||||
abstract setApiKeyClientId(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getApiKeyClientSecret(options?: StorageOptions): Promise<string>;
|
||||
abstract setApiKeyClientSecret(value: string, options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Lifecycle methods
|
||||
abstract init(): Promise<void>;
|
||||
abstract clean(options?: StorageOptions): Promise<void>;
|
||||
|
||||
// Additional state methods
|
||||
abstract getLocale(options?: StorageOptions): Promise<string>;
|
||||
abstract setLocale(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getInstalledVersion(options?: StorageOptions): Promise<string>;
|
||||
abstract setInstalledVersion(value: string, options?: StorageOptions): Promise<void>;
|
||||
abstract getIsAuthenticated(options?: StorageOptions): Promise<boolean>;
|
||||
abstract getEntityId(options?: StorageOptions): Promise<string>;
|
||||
abstract setEntityId(value: string, options?: StorageOptions): Promise<void>;
|
||||
}
|
||||
|
||||
25
src/abstractions/token.service.ts
Normal file
25
src/abstractions/token.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DecodedToken } from "@/src/utils/jwt.util";
|
||||
|
||||
export abstract class TokenService {
|
||||
// Token storage
|
||||
abstract setTokens(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret?: [string, string],
|
||||
): Promise<void>;
|
||||
abstract getToken(): Promise<string | null>;
|
||||
abstract getRefreshToken(): Promise<string | null>;
|
||||
abstract clearToken(): Promise<void>;
|
||||
|
||||
// API key authentication
|
||||
abstract getClientId(): Promise<string | null>;
|
||||
abstract getClientSecret(): Promise<string | null>;
|
||||
|
||||
// Two-factor token (rarely used)
|
||||
abstract getTwoFactorToken(): Promise<string | null>;
|
||||
abstract clearTwoFactorToken(): Promise<void>;
|
||||
|
||||
// Token validation (delegates to jwt.util)
|
||||
abstract decodeToken(token?: string): Promise<DecodedToken | null>;
|
||||
abstract tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise<boolean>;
|
||||
}
|
||||
@@ -1,21 +1,64 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { EnvironmentComponent as BaseEnvironmentComponent } from "@/jslib/angular/src/components/environment.component";
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
|
||||
import { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service";
|
||||
import { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-environment",
|
||||
templateUrl: "environment.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class EnvironmentComponent extends BaseEnvironmentComponent {
|
||||
export class EnvironmentComponent implements OnInit {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
identityUrl: string;
|
||||
apiUrl: string;
|
||||
webVaultUrl: string;
|
||||
baseUrl: string;
|
||||
showCustom = false;
|
||||
|
||||
constructor(
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
super(platformUtilsService, environmentService, i18nService);
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Load environment URLs from state
|
||||
const urls = await this.stateService.getEnvironmentUrls();
|
||||
|
||||
this.baseUrl = urls?.base || "";
|
||||
this.webVaultUrl = urls?.webVault || "";
|
||||
this.apiUrl = urls?.api || "";
|
||||
this.identityUrl = urls?.identity || "";
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
const urls: EnvironmentUrls = {
|
||||
base: this.baseUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
};
|
||||
|
||||
await this.environmentService.setUrls(urls);
|
||||
|
||||
// Reload from state to get normalized URLs (with https:// prefix, etc.)
|
||||
const normalizedUrls = await this.stateService.getEnvironmentUrls();
|
||||
this.baseUrl = normalizedUrls?.base || "";
|
||||
this.apiUrl = normalizedUrls?.api || "";
|
||||
this.identityUrl = normalizedUrls?.identity || "";
|
||||
this.webVaultUrl = normalizedUrls?.webVault || "";
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
|
||||
this.onSaved.emit();
|
||||
}
|
||||
|
||||
toggleCustom(): void {
|
||||
this.showCustom = !this.showCustom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
@@ -35,7 +34,6 @@ export class AppComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService,
|
||||
private tokenService: TokenService,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private toastrService: ToastrService,
|
||||
@@ -116,7 +114,7 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
|
||||
private async logOut(expired: boolean) {
|
||||
await this.tokenService.clearToken();
|
||||
await this.stateService.clearAuthTokens();
|
||||
await this.stateService.clean();
|
||||
|
||||
this.authService.logOut(async () => {
|
||||
|
||||
@@ -1,22 +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 { 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 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 { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||
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";
|
||||
@@ -26,32 +28,34 @@ 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 { 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 { StateService } from "../../services/state.service";
|
||||
import { StateMigrationService } from "../../services/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(
|
||||
environmentService: EnvironmentServiceAbstraction,
|
||||
i18nService: I18nServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
stateService: StateServiceAbstraction,
|
||||
cryptoService: CryptoServiceAbstraction,
|
||||
): () => 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);
|
||||
const environmentService = injector.get(EnvironmentServiceAbstraction);
|
||||
|
||||
await stateService.init();
|
||||
await environmentService.setUrlsFromStorage();
|
||||
await (i18nService as I18nService).init();
|
||||
@@ -72,34 +76,34 @@ export function initFactory(
|
||||
if (installAction != null) {
|
||||
await stateService.setInstalledVersion(currentVersion);
|
||||
}
|
||||
|
||||
const containerService = new ContainerService(cryptoService);
|
||||
containerService.attachToWindow(window);
|
||||
};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [JslibServicesModule],
|
||||
imports: [],
|
||||
declarations: [],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
|
||||
useFactory: initFactory,
|
||||
deps: [
|
||||
EnvironmentServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
],
|
||||
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,
|
||||
@@ -129,6 +133,11 @@ export function initFactory(
|
||||
useClass: NodeCryptoFunctionService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AppIdServiceAbstraction,
|
||||
useClass: AppIdService,
|
||||
deps: [StorageServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ApiServiceAbstraction,
|
||||
useFactory: (
|
||||
@@ -177,7 +186,6 @@ export function initFactory(
|
||||
ApiServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
BatchRequestBuilder,
|
||||
SingleRequestBuilder,
|
||||
@@ -186,42 +194,51 @@ 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,
|
||||
],
|
||||
}),
|
||||
// Provide TokenService and EnvironmentService
|
||||
safeProvider({
|
||||
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,
|
||||
deps: [],
|
||||
@@ -235,6 +252,16 @@ export function initFactory(
|
||||
useClass: DefaultDirectoryFactoryService,
|
||||
deps: [LogServiceAbstraction, I18nServiceAbstraction, StateServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ModalService,
|
||||
useClass: ModalService,
|
||||
deps: [ComponentFactoryResolver, ApplicationRef, Injector],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ValidationService,
|
||||
useClass: ValidationService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||
}),
|
||||
] satisfies SafeProvider[],
|
||||
})
|
||||
export class ServicesModule {}
|
||||
|
||||
49
src/bwdc.ts
49
src/bwdc.ts
@@ -1,38 +1,42 @@
|
||||
import * as fs from "fs";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
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 { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||
import { CryptoService } from "@/jslib/common/src/services/crypto.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 packageJson from "../package.json";
|
||||
|
||||
import { DirectoryFactoryService } from "./abstractions/directory-factory.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 { StateService } from "./services/state.service";
|
||||
import { StateMigrationService } from "./services/stateMigration.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");
|
||||
// ESM __dirname polyfill for Node 20
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export class Main {
|
||||
dataFilePath: string;
|
||||
@@ -44,12 +48,10 @@ export class Main {
|
||||
secureStorageService: StorageServiceAbstraction;
|
||||
i18nService: I18nService;
|
||||
platformUtilsService: CliPlatformUtilsService;
|
||||
cryptoService: CryptoService;
|
||||
tokenService: TokenService;
|
||||
appIdService: AppIdService;
|
||||
apiService: NodeApiService;
|
||||
environmentService: EnvironmentService;
|
||||
containerService: ContainerService;
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
syncService: SyncService;
|
||||
@@ -105,29 +107,21 @@ export class Main {
|
||||
this.stateMigrationService = new StateMigrationService(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
|
||||
this.stateService = new StateService(
|
||||
// Use new StateService with flat key-value structure
|
||||
this.stateService = new StateServiceImplementation(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
this.logService,
|
||||
this.stateMigrationService,
|
||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
|
||||
this.cryptoService = new CryptoService(
|
||||
this.cryptoFunctionService,
|
||||
this.platformUtilsService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
);
|
||||
|
||||
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/" +
|
||||
@@ -143,7 +137,6 @@ export class Main {
|
||||
async (expired: boolean) => await this.logout(),
|
||||
customUserAgent,
|
||||
);
|
||||
this.containerService = new ContainerService(this.cryptoService);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.apiService,
|
||||
@@ -167,7 +160,6 @@ export class Main {
|
||||
this.apiService,
|
||||
this.messagingService,
|
||||
this.i18nService,
|
||||
this.environmentService,
|
||||
this.stateService,
|
||||
this.batchRequestBuilder,
|
||||
this.singleRequestBuilder,
|
||||
@@ -183,14 +175,13 @@ export class Main {
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.tokenService.clearToken();
|
||||
await this.stateService.clearAuthTokens();
|
||||
await this.stateService.clean();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
await this.storageService.init();
|
||||
await this.stateService.init();
|
||||
this.containerService.attachToWindow(global);
|
||||
await this.environmentService.setUrlsFromStorage();
|
||||
// Dev Server URLs. Comment out the line above.
|
||||
// this.apiService.setUrls({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as program from "commander";
|
||||
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
import { Response } from "@/jslib/node/src/cli/models/response";
|
||||
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
|
||||
|
||||
@@ -26,9 +26,8 @@ export class ConfigCommand {
|
||||
private sync = new SyncConfiguration();
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
|
||||
@@ -78,9 +77,9 @@ export class ConfigCommand {
|
||||
|
||||
private async setServer(url: string) {
|
||||
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
|
||||
await this.environmentService.setUrls({
|
||||
base: url,
|
||||
});
|
||||
const urls = new EnvironmentUrls();
|
||||
urls.base = url;
|
||||
await this.stateService.setEnvironmentUrls(urls);
|
||||
}
|
||||
|
||||
private async setDirectory(type: string) {
|
||||
|
||||
23
src/main.ts
23
src/main.ts
@@ -1,9 +1,10 @@
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as path from "path";
|
||||
|
||||
import { app } from "electron";
|
||||
import electronReload from "electron-reload";
|
||||
|
||||
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,12 +12,19 @@ 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 { 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 { StateService } from "./services/state.service";
|
||||
import { StateServiceImplementation } from "./services/state-service/state.service";
|
||||
|
||||
// ESM __dirname polyfill for Node 20
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Import electron-reload for dev mode hot reload
|
||||
|
||||
export class Main {
|
||||
logService: ElectronLogService;
|
||||
@@ -50,21 +58,20 @@ export class Main {
|
||||
const watch = args.some((val) => val === "--watch");
|
||||
|
||||
if (watch) {
|
||||
// eslint-disable-next-line
|
||||
require("electron-reload")(__dirname, {});
|
||||
electronReload(__dirname, {});
|
||||
}
|
||||
|
||||
this.logService = new ElectronLogService(null, app.getPath("userData"));
|
||||
this.logService.init();
|
||||
this.i18nService = new I18nService("en", "./locales/");
|
||||
this.storageService = new ElectronStorageService(app.getPath("userData"));
|
||||
this.stateService = new StateService(
|
||||
// Use new StateService with flat key-value structure
|
||||
this.stateService = new StateServiceImplementation(
|
||||
this.storageService,
|
||||
null,
|
||||
this.logService,
|
||||
null,
|
||||
true,
|
||||
new StateFactory(GlobalState, Account),
|
||||
);
|
||||
|
||||
this.windowMain = new WindowMain(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Account as BaseAccount } from "@/jslib/common/src/models/domain/account";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
|
||||
import { EntraIdConfiguration } from "./entraIdConfiguration";
|
||||
@@ -9,18 +7,6 @@ import { OktaConfiguration } from "./oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "./oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "./syncConfiguration";
|
||||
|
||||
export class Account extends BaseAccount {
|
||||
directoryConfigurations?: DirectoryConfigurations = new DirectoryConfigurations();
|
||||
directorySettings: DirectorySettings = new DirectorySettings();
|
||||
clientKeys: ClientKeys = new ClientKeys();
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
super(init);
|
||||
this.directoryConfigurations = init?.directoryConfigurations ?? new DirectoryConfigurations();
|
||||
this.directorySettings = init?.directorySettings ?? new DirectorySettings();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientKeys {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
|
||||
129
src/models/state.model.ts
Normal file
129
src/models/state.model.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// ===================================================================
|
||||
// vNext Storage Keys (Flat key-value structure)
|
||||
// ===================================================================
|
||||
|
||||
export const StorageKeysVNext = {
|
||||
stateVersion: "stateVersion",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
directory_ldap: "directory_ldap",
|
||||
directory_gsuite: "directory_gsuite",
|
||||
directory_entra: "directory_entra",
|
||||
directory_okta: "directory_okta",
|
||||
directory_onelogin: "directory_onelogin",
|
||||
sync: "sync",
|
||||
syncingDir: "syncingDir",
|
||||
|
||||
// Window/Tray settings
|
||||
window: "window",
|
||||
enableAlwaysOnTop: "enableAlwaysOnTop",
|
||||
enableTray: "enableTray",
|
||||
enableMinimizeToTray: "enableMinimizeToTray",
|
||||
enableCloseToTray: "enableCloseToTray",
|
||||
alwaysShowDock: "alwaysShowDock",
|
||||
|
||||
// Environment URLs
|
||||
environmentUrls: "environmentUrls",
|
||||
};
|
||||
|
||||
export const SecureStorageKeysVNext: { [key: string]: any } = {
|
||||
// Directory service credentials
|
||||
ldap: "secret_ldap",
|
||||
gsuite: "secret_gsuite",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "secret_azure",
|
||||
entra: "secret_entra",
|
||||
okta: "secret_okta",
|
||||
oneLogin: "secret_oneLogin",
|
||||
|
||||
// Sync metadata
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
|
||||
// Authentication tokens
|
||||
accessToken: "accessToken",
|
||||
refreshToken: "refreshToken",
|
||||
apiKeyClientId: "apiKeyClientId",
|
||||
apiKeyClientSecret: "apiKeyClientSecret",
|
||||
twoFactorToken: "twoFactorToken",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Legacy Storage Keys (Account-based hierarchy)
|
||||
// ===================================================================
|
||||
|
||||
export const SecureStorageKeysLegacy = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "azureKey",
|
||||
entra: "entraKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
|
||||
export const TempKeys = {
|
||||
tempAccountSettings: "tempAccountSettings",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Migration Storage Keys
|
||||
// ===================================================================
|
||||
|
||||
export const SecureStorageKeysMigration: { [key: string]: any } = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
azure: "azureKey",
|
||||
entra: "entraIdKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
directoryConfigPrefix: "directoryConfig_",
|
||||
sync: "syncConfig",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
};
|
||||
|
||||
export const MigrationKeys: { [key: string]: any } = {
|
||||
entityId: "entityId",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
syncingDir: "syncingDir",
|
||||
syncConfig: "syncConfig",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
export const MigrationStateKeys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
};
|
||||
|
||||
export const MigrationClientKeys: { [key: string]: any } = {
|
||||
clientIdOld: "clientId",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecretOld: "clientSecret",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
};
|
||||
|
||||
// ===================================================================
|
||||
// Shared Constants
|
||||
// ===================================================================
|
||||
|
||||
export const StoredSecurely = "[STORED SECURELY]";
|
||||
@@ -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.environmentService,
|
||||
this.main.i18nService,
|
||||
this.main.stateService,
|
||||
);
|
||||
const command = new ConfigCommand(this.main.stateService, this.main.i18nService);
|
||||
const response = await command.run(setting, value, options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
@@ -2,18 +2,12 @@ import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||
import {
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "@/jslib/common/src/models/domain/account";
|
||||
import { DeviceRequest } from "@/jslib/common/src/models/request/deviceRequest";
|
||||
import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken/apiTokenRequest";
|
||||
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
|
||||
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
||||
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
|
||||
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@@ -57,35 +51,14 @@ export class AuthService {
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
) {
|
||||
const clientId = tokenRequest.clientId;
|
||||
const entityId = clientId.split("organization.")[1];
|
||||
const clientSecret = tokenRequest.clientSecret;
|
||||
const entityId = clientId.split("organization.")[1];
|
||||
|
||||
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);
|
||||
await this.stateService.setOrganizationId(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,11 @@ 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 { 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 { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
|
||||
import { AuthService } from "./auth.service";
|
||||
import { StateService } from "./state.service";
|
||||
|
||||
const clientId = "organization.CLIENT_ID";
|
||||
const clientSecret = "CLIENT_SECRET";
|
||||
@@ -61,38 +55,21 @@ describe("AuthService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
it("sets the organization ID after a successful login", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
await authService.logIn({ clientId, clientSecret });
|
||||
|
||||
expect(stateService.addAccount).toHaveBeenCalledTimes(1);
|
||||
expect(stateService.addAccount).toHaveBeenCalledWith(
|
||||
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
|
||||
expect(stateService.setAccessToken).toHaveBeenCalledWith(accessToken);
|
||||
expect(stateService.setRefreshToken).toHaveBeenCalledWith(refreshToken);
|
||||
|
||||
// Verify API key credentials are saved
|
||||
expect(stateService.setApiKeyClientId).toHaveBeenCalledWith(clientId);
|
||||
expect(stateService.setApiKeyClientSecret).toHaveBeenCalledWith(clientSecret);
|
||||
|
||||
// Verify entity ID and organization ID are saved
|
||||
expect(stateService.setEntityId).toHaveBeenCalledWith("CLIENT_ID");
|
||||
expect(stateService.setOrganizationId).toHaveBeenCalledWith("CLIENT_ID");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
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";
|
||||
import {
|
||||
@@ -10,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";
|
||||
|
||||
import { GSuiteDirectoryService } from "./gsuite-directory.service";
|
||||
|
||||
|
||||
@@ -4,7 +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 { StateService } from "../../abstractions/state.service";
|
||||
import { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { GroupEntry } from "../../models/groupEntry";
|
||||
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from "../../../utils/openldap/config-fixtures";
|
||||
import { groupFixtures } from "../../../utils/openldap/group-fixtures";
|
||||
import { userFixtures } from "../../../utils/openldap/user-fixtures";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { StateService } from "../state.service";
|
||||
|
||||
import { LdapDirectoryService } from "./ldap-directory.service";
|
||||
|
||||
|
||||
78
src/services/environment/environment.service.ts
Normal file
78
src/services/environment/environment.service.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
|
||||
import { EnvironmentService as IEnvironmentService } from "@/src/abstractions/environment.service";
|
||||
import { StateService } from "@/src/abstractions/state.service";
|
||||
|
||||
export class EnvironmentService implements IEnvironmentService {
|
||||
private readonly DEFAULT_URLS = {
|
||||
api: "https://api.bitwarden.com",
|
||||
identity: "https://identity.bitwarden.com",
|
||||
webVault: "https://vault.bitwarden.com",
|
||||
};
|
||||
|
||||
private urls: EnvironmentUrls = new EnvironmentUrls();
|
||||
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async setUrls(urls: EnvironmentUrls): Promise<void> {
|
||||
// Normalize URLs: trim whitespace, remove trailing slashes, add https:// if missing
|
||||
const normalized = new EnvironmentUrls();
|
||||
|
||||
for (const [key, value] of Object.entries(urls)) {
|
||||
if (!value || typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let url = value.trim();
|
||||
url = url.replace(/\/+$/, ""); // Remove trailing slashes
|
||||
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
normalized[key as keyof EnvironmentUrls] = url;
|
||||
}
|
||||
|
||||
this.urls = normalized;
|
||||
await this.stateService.setEnvironmentUrls(normalized);
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const stored = await this.stateService.getEnvironmentUrls();
|
||||
this.urls = stored ?? new EnvironmentUrls();
|
||||
}
|
||||
|
||||
hasBaseUrl(): boolean {
|
||||
return !!this.urls.base;
|
||||
}
|
||||
|
||||
getApiUrl(): string {
|
||||
if (this.urls.api) {
|
||||
return this.urls.api;
|
||||
}
|
||||
if (this.urls.base) {
|
||||
return this.urls.base + "/api";
|
||||
}
|
||||
return this.DEFAULT_URLS.api;
|
||||
}
|
||||
|
||||
getIdentityUrl(): string {
|
||||
if (this.urls.identity) {
|
||||
return this.urls.identity;
|
||||
}
|
||||
if (this.urls.base) {
|
||||
return this.urls.base + "/identity";
|
||||
}
|
||||
return this.DEFAULT_URLS.identity;
|
||||
}
|
||||
|
||||
getWebVaultUrl(): string {
|
||||
if (this.urls.webVault) {
|
||||
return this.urls.webVault;
|
||||
}
|
||||
if (this.urls.base) {
|
||||
return this.urls.base;
|
||||
}
|
||||
return this.DEFAULT_URLS.webVault;
|
||||
}
|
||||
}
|
||||
881
src/services/state-service/state.service.spec.ts
Normal file
881
src/services/state-service/state.service.spec.ts
Normal file
@@ -0,0 +1,881 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
import { EnvironmentUrls } from "@/jslib/common/src/models/domain/environmentUrls";
|
||||
|
||||
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 {
|
||||
SecureStorageKeysVNext as SecureStorageKeys,
|
||||
StorageKeysVNext as StorageKeys,
|
||||
StoredSecurely,
|
||||
} from "@/src/models/state.model";
|
||||
import { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
import { StateServiceImplementation } from "./state.service";
|
||||
import { StateMigrationService } from "./stateMigration.service";
|
||||
|
||||
describe("StateServiceImplementation", () => {
|
||||
let storageService: MockProxy<StorageService>;
|
||||
let secureStorageService: MockProxy<StorageService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateMigrationService: MockProxy<StateMigrationService>;
|
||||
let stateService: StateServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = mock<StorageService>();
|
||||
secureStorageService = mock<StorageService>();
|
||||
logService = mock<LogService>();
|
||||
stateMigrationService = mock<StateMigrationService>();
|
||||
|
||||
stateService = new StateServiceImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
true, // useSecureStorageForSecrets
|
||||
);
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("should run migration if needed", async () => {
|
||||
stateMigrationService.needsMigration.mockResolvedValue(true);
|
||||
|
||||
await stateService.init();
|
||||
|
||||
expect(stateMigrationService.needsMigration).toHaveBeenCalled();
|
||||
expect(stateMigrationService.migrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not run migration if not needed", async () => {
|
||||
stateMigrationService.needsMigration.mockResolvedValue(false);
|
||||
|
||||
await stateService.init();
|
||||
|
||||
expect(stateMigrationService.needsMigration).toHaveBeenCalled();
|
||||
expect(stateMigrationService.migrate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clean", () => {
|
||||
it("should clear all directory settings and configurations", async () => {
|
||||
await stateService.clean();
|
||||
|
||||
// Verify all directory types are cleared
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.directoryType, null);
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.organizationId, null);
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.sync, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Directory Type", () => {
|
||||
it("should store and retrieve directory type", async () => {
|
||||
storageService.get.mockResolvedValue(DirectoryType.Ldap);
|
||||
|
||||
await stateService.setDirectoryType(DirectoryType.Ldap);
|
||||
const result = await stateService.getDirectoryType();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(
|
||||
StorageKeys.directoryType,
|
||||
DirectoryType.Ldap,
|
||||
);
|
||||
expect(result).toBe(DirectoryType.Ldap);
|
||||
});
|
||||
|
||||
it("should return null when directory type is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getDirectoryType();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Organization Id", () => {
|
||||
it("should store and retrieve organization ID", async () => {
|
||||
const orgId = "test-org-123";
|
||||
|
||||
storageService.get.mockResolvedValue(orgId);
|
||||
|
||||
await stateService.setOrganizationId(orgId);
|
||||
const result = await stateService.getOrganizationId();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.organizationId, orgId);
|
||||
expect(result).toBe(orgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LDAP Configuration", () => {
|
||||
it("should store and retrieve LDAP configuration with secrets in secure storage", async () => {
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: "secret-password",
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("secret-password");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
password: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Ldap, config);
|
||||
const result = await stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||
|
||||
// Verify password is stored in secure storage
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
|
||||
// Verify configuration is stored
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
|
||||
// Verify retrieved config has real password from secure storage
|
||||
expect(result?.password).toBe("secret-password");
|
||||
});
|
||||
|
||||
it("should return null when LDAP configuration is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLdapConfiguration();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle null password in LDAP configuration", async () => {
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: null,
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Ldap, config);
|
||||
|
||||
// Null passwords should call remove on the secure storage secret key
|
||||
expect(secureStorageService.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GSuite Configuration", () => {
|
||||
it("should store and retrieve GSuite configuration with privateKey in secure storage", async () => {
|
||||
const config: GSuiteConfiguration = {
|
||||
domain: "example.com",
|
||||
clientEmail: "service@example.com",
|
||||
adminUser: "admin@example.com",
|
||||
privateKey: "private-key-content",
|
||||
customer: "customer-id",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("private-key-content");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
privateKey: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.GSuite, config);
|
||||
const result = await stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.privateKey).toBe("private-key-content");
|
||||
});
|
||||
|
||||
it("should handle null privateKey in GSuite configuration", async () => {
|
||||
const config: GSuiteConfiguration = {
|
||||
domain: "example.com",
|
||||
clientEmail: "service@example.com",
|
||||
adminUser: "admin@example.com",
|
||||
privateKey: null,
|
||||
customer: "customer-id",
|
||||
};
|
||||
|
||||
await stateService.setDirectory(DirectoryType.GSuite, config);
|
||||
|
||||
// Null privateKey should call remove on the secure storage secret key
|
||||
expect(secureStorageService.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Entra ID Configuration", () => {
|
||||
it("should store and retrieve Entra ID configuration with key in secure storage", async () => {
|
||||
const config: EntraIdConfiguration = {
|
||||
identityAuthority: "https://login.microsoftonline.com",
|
||||
tenant: "tenant-id",
|
||||
applicationId: "app-id",
|
||||
key: "secret-key",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("secret-key");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
key: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.EntraID, config);
|
||||
const result = await stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.key).toBe("secret-key");
|
||||
});
|
||||
|
||||
it("should maintain backwards compatibility with Azure key storage", async () => {
|
||||
const config: EntraIdConfiguration = {
|
||||
identityAuthority: "https://login.microsoftonline.com",
|
||||
tenant: "tenant-id",
|
||||
applicationId: "app-id",
|
||||
key: StoredSecurely,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(config);
|
||||
secureStorageService.get.mockResolvedValueOnce(null); // entra key not found
|
||||
secureStorageService.get.mockResolvedValueOnce("azure-secret-key"); // fallback to azure key
|
||||
|
||||
const result = await stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID);
|
||||
|
||||
expect(secureStorageService.get).toHaveBeenCalled();
|
||||
expect(result?.key).toBe("azure-secret-key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Okta Configuration", () => {
|
||||
it("should store and retrieve Okta configuration with token in secure storage", async () => {
|
||||
const config: OktaConfiguration = {
|
||||
orgUrl: "https://example.okta.com",
|
||||
token: "okta-token",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("okta-token");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
token: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.Okta, config);
|
||||
const result = await stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.token).toBe("okta-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OneLogin Configuration", () => {
|
||||
it("should store and retrieve OneLogin configuration with clientSecret in secure storage", async () => {
|
||||
const config: OneLoginConfiguration = {
|
||||
region: "us",
|
||||
clientId: "client-id",
|
||||
clientSecret: "client-secret",
|
||||
};
|
||||
|
||||
secureStorageService.get.mockResolvedValue("client-secret");
|
||||
storageService.get.mockResolvedValue({
|
||||
...config,
|
||||
clientSecret: StoredSecurely,
|
||||
});
|
||||
|
||||
await stateService.setDirectory(DirectoryType.OneLogin, config);
|
||||
const result = await stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalled();
|
||||
expect(result?.clientSecret).toBe("client-secret");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync Configuration", () => {
|
||||
it("should store and retrieve sync configuration", async () => {
|
||||
const syncConfig: SyncConfiguration = {
|
||||
users: true,
|
||||
groups: true,
|
||||
interval: 5,
|
||||
userFilter: null,
|
||||
groupFilter: null,
|
||||
removeDisabled: true,
|
||||
overwriteExisting: false,
|
||||
largeImport: false,
|
||||
groupObjectClass: null,
|
||||
userObjectClass: null,
|
||||
groupPath: null,
|
||||
userPath: null,
|
||||
groupNameAttribute: null,
|
||||
userEmailAttribute: null,
|
||||
memberAttribute: "member",
|
||||
creationDateAttribute: "whenCreated",
|
||||
revisionDateAttribute: "whenChanged",
|
||||
useEmailPrefixSuffix: false,
|
||||
emailPrefixAttribute: null,
|
||||
emailSuffix: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(syncConfig);
|
||||
|
||||
await stateService.setSync(syncConfig);
|
||||
const result = await stateService.getSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.sync, syncConfig);
|
||||
expect(result).toEqual(syncConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync Settings", () => {
|
||||
it("should clear sync settings when clearSyncSettings is called", async () => {
|
||||
await stateService.clearSyncSettings(false);
|
||||
|
||||
// Should set delta and sync values to null
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should clear lastSyncHash when hashToo is true", async () => {
|
||||
await stateService.clearSyncSettings(true);
|
||||
|
||||
// Should set all values including lastSyncHash to null
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not clear lastSyncHash when hashToo is false", async () => {
|
||||
await stateService.clearSyncSettings(false);
|
||||
|
||||
// Should set delta and sync values but not lastSyncHash
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Last Sync Hash", () => {
|
||||
it("should store and retrieve last sync hash", async () => {
|
||||
const hash = "hash";
|
||||
|
||||
storageService.get.mockResolvedValue(hash);
|
||||
|
||||
await stateService.setLastSyncHash(hash);
|
||||
const result = await stateService.getLastSyncHash();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Delta Tokens", () => {
|
||||
it("should store and retrieve user delta token", async () => {
|
||||
const token = "user-delta-token";
|
||||
|
||||
storageService.get.mockResolvedValue(token);
|
||||
|
||||
await stateService.setUserDelta(token);
|
||||
const result = await stateService.getUserDelta();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(token);
|
||||
});
|
||||
|
||||
it("should store and retrieve group delta token", async () => {
|
||||
const token = "group-delta-token";
|
||||
|
||||
storageService.get.mockResolvedValue(token);
|
||||
|
||||
await stateService.setGroupDelta(token);
|
||||
const result = await stateService.getGroupDelta();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result).toBe(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Last Sync Timestamps", () => {
|
||||
it("should store and retrieve last user sync timestamp", async () => {
|
||||
const timestamp = new Date("2024-01-01T00:00:00Z");
|
||||
|
||||
storageService.get.mockResolvedValue(timestamp.toISOString());
|
||||
|
||||
await stateService.setLastUserSync(timestamp);
|
||||
const result = await stateService.getLastUserSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result?.toISOString()).toBe(timestamp.toISOString());
|
||||
});
|
||||
|
||||
it("should store and retrieve last group sync timestamp", async () => {
|
||||
const timestamp = new Date("2024-01-01T00:00:00Z");
|
||||
|
||||
storageService.get.mockResolvedValue(timestamp.toISOString());
|
||||
|
||||
await stateService.setLastGroupSync(timestamp);
|
||||
const result = await stateService.getLastGroupSync();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalled();
|
||||
expect(result?.toISOString()).toBe(timestamp.toISOString());
|
||||
});
|
||||
|
||||
it("should return null when last user sync timestamp is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLastUserSync();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when last group sync timestamp is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getLastGroupSync();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Secure Storage Flag", () => {
|
||||
it("should not separate secrets when useSecureStorageForSecrets is false", async () => {
|
||||
const insecureStateService = new StateServiceImplementation(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
false, // useSecureStorageForSecrets = false
|
||||
);
|
||||
|
||||
const config: LdapConfiguration = {
|
||||
ssl: true,
|
||||
startTls: false,
|
||||
tlsCaPath: null,
|
||||
sslAllowUnauthorized: false,
|
||||
sslCertPath: null,
|
||||
sslKeyPath: null,
|
||||
sslCaPath: null,
|
||||
hostname: "ldap.example.com",
|
||||
port: 636,
|
||||
domain: null,
|
||||
rootPath: null,
|
||||
ad: true,
|
||||
username: "admin",
|
||||
password: "secret-password",
|
||||
currentUser: false,
|
||||
pagedSearch: true,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(config);
|
||||
|
||||
// When useSecureStorageForSecrets is false, setDirectory doesn't process secrets
|
||||
await insecureStateService.setDirectory(DirectoryType.Ldap, config);
|
||||
|
||||
// Retrieve config - should return password as-is from storage (not from secure storage)
|
||||
const result = await insecureStateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
|
||||
|
||||
// Password should be retrieved directly from storage, not secure storage
|
||||
expect(result?.password).toBe("secret-password");
|
||||
expect(secureStorageService.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Window Settings", () => {
|
||||
it("should store and retrieve window state", async () => {
|
||||
const windowState = {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
x: 100,
|
||||
y: 100,
|
||||
isMaximized: false,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(windowState);
|
||||
|
||||
await stateService.setWindow(windowState);
|
||||
const result = await stateService.getWindow();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.window, windowState);
|
||||
expect(result).toEqual(windowState);
|
||||
});
|
||||
|
||||
it("should return null when window state is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getWindow();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should store and retrieve enableAlwaysOnTop setting", async () => {
|
||||
storageService.get.mockResolvedValue(true);
|
||||
|
||||
await stateService.setEnableAlwaysOnTop(true);
|
||||
const result = await stateService.getEnableAlwaysOnTop();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.enableAlwaysOnTop, true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when enableAlwaysOnTop is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getEnableAlwaysOnTop();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tray Settings", () => {
|
||||
it("should store and retrieve enableTray setting", async () => {
|
||||
storageService.get.mockResolvedValue(true);
|
||||
|
||||
await stateService.setEnableTray(true);
|
||||
const result = await stateService.getEnableTray();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.enableTray, true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when enableTray is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getEnableTray();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should store and retrieve enableMinimizeToTray setting", async () => {
|
||||
storageService.get.mockResolvedValue(true);
|
||||
|
||||
await stateService.setEnableMinimizeToTray(true);
|
||||
const result = await stateService.getEnableMinimizeToTray();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.enableMinimizeToTray, true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when enableMinimizeToTray is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getEnableMinimizeToTray();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should store and retrieve enableCloseToTray setting", async () => {
|
||||
storageService.get.mockResolvedValue(true);
|
||||
|
||||
await stateService.setEnableCloseToTray(true);
|
||||
const result = await stateService.getEnableCloseToTray();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.enableCloseToTray, true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when enableCloseToTray is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getEnableCloseToTray();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should store and retrieve alwaysShowDock setting", async () => {
|
||||
storageService.get.mockResolvedValue(true);
|
||||
|
||||
await stateService.setAlwaysShowDock(true);
|
||||
const result = await stateService.getAlwaysShowDock();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.alwaysShowDock, true);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when alwaysShowDock is not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getAlwaysShowDock();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environment URLs", () => {
|
||||
it("should store and retrieve environment URLs", async () => {
|
||||
const urls: EnvironmentUrls = {
|
||||
base: "https://vault.example.com",
|
||||
api: "https://api.example.com",
|
||||
identity: "https://identity.example.com",
|
||||
webVault: "https://vault.example.com",
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(urls);
|
||||
|
||||
await stateService.setEnvironmentUrls(urls);
|
||||
const result = await stateService.getEnvironmentUrls();
|
||||
|
||||
expect(storageService.save).toHaveBeenCalledWith(StorageKeys.environmentUrls, urls);
|
||||
expect(result).toEqual(urls);
|
||||
});
|
||||
|
||||
it("should return null when environment URLs are not set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getEnvironmentUrls();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return API URL from explicit api property", async () => {
|
||||
const urls: EnvironmentUrls = {
|
||||
base: null,
|
||||
api: "https://api.example.com",
|
||||
identity: null,
|
||||
webVault: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(urls);
|
||||
|
||||
const result = await stateService.getApiUrl();
|
||||
|
||||
expect(result).toBe("https://api.example.com");
|
||||
});
|
||||
|
||||
it("should return API URL derived from base URL", async () => {
|
||||
const urls: EnvironmentUrls = {
|
||||
base: "https://vault.example.com",
|
||||
api: null,
|
||||
identity: null,
|
||||
webVault: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(urls);
|
||||
|
||||
const result = await stateService.getApiUrl();
|
||||
|
||||
expect(result).toBe("https://vault.example.com/api");
|
||||
});
|
||||
|
||||
it("should return default API URL when no URLs are set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getApiUrl();
|
||||
|
||||
expect(result).toBe("https://api.bitwarden.com");
|
||||
});
|
||||
|
||||
it("should return Identity URL from explicit identity property", async () => {
|
||||
const urls: EnvironmentUrls = {
|
||||
base: null,
|
||||
api: null,
|
||||
identity: "https://identity.example.com",
|
||||
webVault: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(urls);
|
||||
|
||||
const result = await stateService.getIdentityUrl();
|
||||
|
||||
expect(result).toBe("https://identity.example.com");
|
||||
});
|
||||
|
||||
it("should return Identity URL derived from base URL", async () => {
|
||||
const urls: EnvironmentUrls = {
|
||||
base: "https://vault.example.com",
|
||||
api: null,
|
||||
identity: null,
|
||||
webVault: null,
|
||||
};
|
||||
|
||||
storageService.get.mockResolvedValue(urls);
|
||||
|
||||
const result = await stateService.getIdentityUrl();
|
||||
|
||||
expect(result).toBe("https://vault.example.com/identity");
|
||||
});
|
||||
|
||||
it("should return default Identity URL when no URLs are set", async () => {
|
||||
storageService.get.mockResolvedValue(null);
|
||||
|
||||
const result = await stateService.getIdentityUrl();
|
||||
|
||||
expect(result).toBe("https://identity.bitwarden.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Token Management", () => {
|
||||
it("should clear all auth tokens", async () => {
|
||||
await stateService.clearAuthTokens();
|
||||
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.accessToken);
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.refreshToken);
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.apiKeyClientId);
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(
|
||||
SecureStorageKeys.apiKeyClientSecret,
|
||||
);
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.twoFactorToken);
|
||||
});
|
||||
|
||||
it("should remove exactly 5 token types", async () => {
|
||||
await stateService.clearAuthTokens();
|
||||
|
||||
// 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(SecureStorageKeys.accessToken);
|
||||
});
|
||||
|
||||
it("should set access token in secure storage", async () => {
|
||||
const token = "test-access-token";
|
||||
|
||||
await stateService.setAccessToken(token);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalledWith(
|
||||
SecureStorageKeys.accessToken,
|
||||
token,
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove access token when set to null", async () => {
|
||||
await stateService.setAccessToken(null);
|
||||
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.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(SecureStorageKeys.refreshToken);
|
||||
});
|
||||
|
||||
it("should set refresh token in secure storage", async () => {
|
||||
const token = "test-refresh-token";
|
||||
|
||||
await stateService.setRefreshToken(token);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalledWith(
|
||||
SecureStorageKeys.refreshToken,
|
||||
token,
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove refresh token when set to null", async () => {
|
||||
await stateService.setRefreshToken(null);
|
||||
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.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(SecureStorageKeys.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(
|
||||
SecureStorageKeys.apiKeyClientId,
|
||||
clientId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove API key client ID when set to null", async () => {
|
||||
await stateService.setApiKeyClientId(null);
|
||||
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(SecureStorageKeys.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(SecureStorageKeys.apiKeyClientSecret);
|
||||
});
|
||||
|
||||
it("should set API key client secret in secure storage", async () => {
|
||||
const clientSecret = "test-secret";
|
||||
|
||||
await stateService.setApiKeyClientSecret(clientSecret);
|
||||
|
||||
expect(secureStorageService.save).toHaveBeenCalledWith(
|
||||
SecureStorageKeys.apiKeyClientSecret,
|
||||
clientSecret,
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove API key client secret when set to null", async () => {
|
||||
await stateService.setApiKeyClientSecret(null);
|
||||
|
||||
expect(secureStorageService.remove).toHaveBeenCalledWith(
|
||||
SecureStorageKeys.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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
569
src/services/state-service/state.service.ts
Normal file
569
src/services/state-service/state.service.ts
Normal file
@@ -0,0 +1,569 @@
|
||||
import { LogService } from "@/jslib/common/src/abstractions/log.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 { StateService as StateServiceAbstraction } from "@/src/abstractions/state.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";
|
||||
|
||||
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,
|
||||
) {}
|
||||
|
||||
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(SecureStorageKeys.accessToken);
|
||||
await this.secureStorageService.remove(SecureStorageKeys.refreshToken);
|
||||
await this.secureStorageService.remove(SecureStorageKeys.apiKeyClientId);
|
||||
await this.secureStorageService.remove(SecureStorageKeys.apiKeyClientSecret);
|
||||
await this.secureStorageService.remove(SecureStorageKeys.twoFactorToken);
|
||||
}
|
||||
|
||||
async getAccessToken(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.accessToken);
|
||||
}
|
||||
|
||||
async setAccessToken(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.accessToken);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.accessToken, value);
|
||||
}
|
||||
}
|
||||
|
||||
async getRefreshToken(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.refreshToken);
|
||||
}
|
||||
|
||||
async setRefreshToken(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.refreshToken);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.refreshToken, value);
|
||||
}
|
||||
}
|
||||
|
||||
async getApiKeyClientId(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.apiKeyClientId);
|
||||
}
|
||||
|
||||
async setApiKeyClientId(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.apiKeyClientId);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.apiKeyClientId, value);
|
||||
}
|
||||
}
|
||||
|
||||
async getApiKeyClientSecret(options?: StorageOptions): Promise<string> {
|
||||
return await this.secureStorageService.get<string>(SecureStorageKeys.apiKeyClientSecret);
|
||||
}
|
||||
|
||||
async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise<void> {
|
||||
if (value == null) {
|
||||
await this.secureStorageService.remove(SecureStorageKeys.apiKeyClientSecret);
|
||||
} else {
|
||||
await this.secureStorageService.save(SecureStorageKeys.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";
|
||||
376
src/services/state-service/stateMigration.service.ts
Normal file
376
src/services/state-service/stateMigration.service.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
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 { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
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";
|
||||
import { OktaConfiguration } from "@/src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "@/src/models/oneLoginConfiguration";
|
||||
import {
|
||||
MigrationClientKeys as ClientKeys,
|
||||
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 {
|
||||
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) {
|
||||
switch (currentStateVersion) {
|
||||
case StateVersion.One:
|
||||
await this.migrateClientKeys();
|
||||
await this.migrateStateFrom1To2();
|
||||
break;
|
||||
case StateVersion.Two:
|
||||
await this.migrateStateFrom2To3();
|
||||
break;
|
||||
case StateVersion.Three:
|
||||
await this.migrateStateFrom3To4();
|
||||
break;
|
||||
case StateVersion.Four:
|
||||
await this.migrateStateFrom4To5();
|
||||
break;
|
||||
}
|
||||
currentStateVersion += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this migration when we are confident existing api keys are all migrated. Probably 1-2 releases.
|
||||
protected async migrateClientKeys() {
|
||||
const oldClientId = await this.storageService.get<string>(ClientKeys.clientIdOld);
|
||||
const oldClientSecret = await this.storageService.get<string>(ClientKeys.clientSecretOld);
|
||||
|
||||
if (oldClientId != null) {
|
||||
await this.storageService.save(ClientKeys.clientId, oldClientId);
|
||||
await this.storageService.remove(ClientKeys.clientIdOld);
|
||||
}
|
||||
|
||||
if (oldClientSecret != null) {
|
||||
await this.storageService.save(ClientKeys.clientSecret, oldClientSecret);
|
||||
await this.storageService.remove(ClientKeys.clientSecretOld);
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrateStateFrom1To2(useSecureStorageForSecrets = true): Promise<void> {
|
||||
// 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);
|
||||
|
||||
// Setup reusable method for clearing keys
|
||||
const clearDirectoryConnectorV1Keys = async () => {
|
||||
for (const key in Keys) {
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
for (const directoryType in DirectoryType) {
|
||||
if (directoryType == null) {
|
||||
continue;
|
||||
}
|
||||
await this.set(SecureStorageKeys.directoryConfigPrefix + directoryType, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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 = {
|
||||
ldap: await getDirectoryConfig<LdapConfiguration>(DirectoryType.Ldap),
|
||||
gsuite: await getDirectoryConfig<GSuiteConfiguration>(DirectoryType.GSuite),
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: await getDirectoryConfig<EntraIdConfiguration>(DirectoryType.EntraID),
|
||||
entra: await getDirectoryConfig<EntraIdConfiguration>(DirectoryType.EntraID),
|
||||
okta: await getDirectoryConfig<OktaConfiguration>(DirectoryType.Okta),
|
||||
oneLogin: await getDirectoryConfig<OneLoginConfiguration>(DirectoryType.OneLogin),
|
||||
};
|
||||
|
||||
const directorySettings: DirectorySettings = {
|
||||
directoryType: await this.get<DirectoryType>(Keys.directoryType),
|
||||
organizationId: await this.get<string>(Keys.organizationId),
|
||||
lastUserSync: await this.get<Date>(Keys.lastUserSync),
|
||||
lastGroupSync: await this.get<Date>(Keys.lastGroupSync),
|
||||
lastSyncHash: await this.get<string>(Keys.lastSyncHash),
|
||||
syncingDir: await this.get<boolean>(Keys.syncingDir),
|
||||
sync: await this.get<SyncConfiguration>(Keys.syncConfig),
|
||||
userDelta: await this.get<string>(Keys.userDelta),
|
||||
groupDelta: await this.get<string>(Keys.groupDelta),
|
||||
};
|
||||
|
||||
// (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<any>(userId);
|
||||
account.directoryConfigurations = directoryConfigs;
|
||||
account.directorySettings = directorySettings;
|
||||
account.userId = userId;
|
||||
account.entityId = userId;
|
||||
account.apiKeyClientId = clientId;
|
||||
account.apiKeyClientSecret = clientSecret;
|
||||
|
||||
await this.set(userId, account);
|
||||
await clearDirectoryConnectorV1Keys();
|
||||
|
||||
if (useSecureStorageForSecrets) {
|
||||
for (const key in SecureStorageKeys) {
|
||||
if (await this.secureStorageService.has(SecureStorageKeys[key])) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}_${SecureStorageKeys[key]}`,
|
||||
await this.secureStorageService.get(SecureStorageKeys[key]),
|
||||
);
|
||||
await this.secureStorageService.remove(SecureStorageKeys[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
// Placeholder migration for v3→v4 (no changes needed for DC)
|
||||
const globals = await this.getGlobals();
|
||||
if (globals) {
|
||||
globals.stateVersion = StateVersion.Four;
|
||||
await this.set(StateKeys.global, globals);
|
||||
} else {
|
||||
await this.set(StorageKeysVNext.stateVersion, StateVersion.Four);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from State v4 (Account-based hierarchy) to v5 (flat key-value structure)
|
||||
*
|
||||
* This is a clean break from the Account-based structure. Data is extracted from
|
||||
* the account and saved into flat keys for simpler access.
|
||||
*/
|
||||
protected async migrateStateFrom4To5(useSecureStorageForSecrets = true): Promise<void> {
|
||||
const clientId = await this.storageService.get<string>("activeUserId");
|
||||
const account = await this.get<any>(clientId);
|
||||
|
||||
if (!account) {
|
||||
// No account data found, just update version
|
||||
await this.set(StorageKeysVNext.stateVersion, StateVersion.Five);
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate directory configurations to flat structure
|
||||
if (account.directoryConfigurations) {
|
||||
if (account.directoryConfigurations.ldap) {
|
||||
await this.set(StorageKeysVNext.directory_ldap, account.directoryConfigurations.ldap);
|
||||
}
|
||||
if (account.directoryConfigurations.gsuite) {
|
||||
await this.set(StorageKeysVNext.directory_gsuite, account.directoryConfigurations.gsuite);
|
||||
}
|
||||
if (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(StorageKeysVNext.directory_entra, account.directoryConfigurations.azure);
|
||||
}
|
||||
if (account.directoryConfigurations.okta) {
|
||||
await this.set(StorageKeysVNext.directory_okta, account.directoryConfigurations.okta);
|
||||
}
|
||||
if (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(StorageKeysVNext.organizationId, account.directorySettings.organizationId);
|
||||
}
|
||||
if (account.directorySettings.directoryType != null) {
|
||||
await this.set(StorageKeysVNext.directoryType, account.directorySettings.directoryType);
|
||||
}
|
||||
if (account.directorySettings.sync) {
|
||||
await this.set(StorageKeysVNext.sync, account.directorySettings.sync);
|
||||
}
|
||||
if (account.directorySettings.lastUserSync) {
|
||||
await this.set(SecureStorageKeysVNext.lastUserSync, account.directorySettings.lastUserSync);
|
||||
}
|
||||
if (account.directorySettings.lastGroupSync) {
|
||||
await this.set(
|
||||
SecureStorageKeysVNext.lastGroupSync,
|
||||
account.directorySettings.lastGroupSync,
|
||||
);
|
||||
}
|
||||
if (account.directorySettings.lastSyncHash) {
|
||||
await this.set(SecureStorageKeysVNext.lastSyncHash, account.directorySettings.lastSyncHash);
|
||||
}
|
||||
if (account.directorySettings.userDelta) {
|
||||
await this.set(SecureStorageKeysVNext.userDelta, account.directorySettings.userDelta);
|
||||
}
|
||||
if (account.directorySettings.groupDelta) {
|
||||
await this.set(SecureStorageKeysVNext.groupDelta, account.directorySettings.groupDelta);
|
||||
}
|
||||
if (account.directorySettings.syncingDir != null) {
|
||||
await this.set(StorageKeysVNext.syncingDir, account.directorySettings.syncingDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate secrets from {userId}_* to secret_* pattern
|
||||
if (useSecureStorageForSecrets) {
|
||||
const oldSecretKeys = [
|
||||
{ old: `${clientId}_${SecureStorageKeys.ldap}`, new: SecureStorageKeysVNext.ldap },
|
||||
{ old: `${clientId}_${SecureStorageKeys.gsuite}`, new: SecureStorageKeysVNext.gsuite },
|
||||
{ old: `${clientId}_${SecureStorageKeys.azure}`, new: SecureStorageKeysVNext.azure },
|
||||
{ old: `${clientId}_${SecureStorageKeys.entra}`, new: SecureStorageKeysVNext.entra },
|
||||
{ old: `${clientId}_${SecureStorageKeys.okta}`, new: SecureStorageKeysVNext.okta },
|
||||
{ old: `${clientId}_${SecureStorageKeys.oneLogin}`, new: SecureStorageKeysVNext.oneLogin },
|
||||
];
|
||||
|
||||
for (const { old: oldKey, new: newKey } of oldSecretKeys) {
|
||||
if (await this.secureStorageService.has(oldKey)) {
|
||||
const value = await this.secureStorageService.get(oldKey);
|
||||
if (value) {
|
||||
await this.secureStorageService.save(newKey, value);
|
||||
}
|
||||
// @TODO Keep old key for now - will remove in future release
|
||||
// await this.secureStorageService.remove(oldKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate window/tray settings from globals object
|
||||
const globals = await this.getGlobals();
|
||||
if (globals) {
|
||||
if (globals.window) {
|
||||
await this.set(StorageKeysVNext.window, globals.window);
|
||||
}
|
||||
if (globals.enableAlwaysOnTop !== undefined) {
|
||||
await this.set(StorageKeysVNext.enableAlwaysOnTop, globals.enableAlwaysOnTop);
|
||||
}
|
||||
if (globals.enableTray !== undefined) {
|
||||
await this.set(StorageKeysVNext.enableTray, globals.enableTray);
|
||||
}
|
||||
if (globals.enableMinimizeToTray !== undefined) {
|
||||
await this.set(StorageKeysVNext.enableMinimizeToTray, globals.enableMinimizeToTray);
|
||||
}
|
||||
if (globals.enableCloseToTray !== undefined) {
|
||||
await this.set(StorageKeysVNext.enableCloseToTray, globals.enableCloseToTray);
|
||||
}
|
||||
if (globals.alwaysShowDock !== undefined) {
|
||||
await this.set(StorageKeysVNext.alwaysShowDock, globals.alwaysShowDock);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate environment URLs from account settings
|
||||
if (account.settings?.environmentUrls) {
|
||||
await this.set(StorageKeysVNext.environmentUrls, account.settings.environmentUrls);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -1,613 +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 { 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 { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
const SecureStorageKeys = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: "azureKey",
|
||||
entra: "entraKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
};
|
||||
|
||||
const keys = {
|
||||
tempAccountSettings: "tempAccountSettings",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
const StoredSecurely = "[STORED SECURELY]";
|
||||
|
||||
export class StateService
|
||||
extends BaseStateService<GlobalState, Account>
|
||||
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 getDirectory<T extends IConfiguration>(type: DirectoryType): Promise<T> {
|
||||
const config = await this.getConfiguration(type);
|
||||
if (config == null) {
|
||||
return config as T;
|
||||
}
|
||||
|
||||
if (this.useSecureStorageForSecrets) {
|
||||
// Do not introduce secrets into the in-memory account object
|
||||
const configWithSecrets = Object.assign({}, config);
|
||||
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
(configWithSecrets as any).password = await this.getLdapKey();
|
||||
break;
|
||||
case DirectoryType.EntraID:
|
||||
(configWithSecrets as any).key = await this.getEntraKey();
|
||||
break;
|
||||
case DirectoryType.Okta:
|
||||
(configWithSecrets as any).token = await this.getOktaKey();
|
||||
break;
|
||||
case DirectoryType.GSuite:
|
||||
(configWithSecrets as any).privateKey = await this.getGsuiteKey();
|
||||
break;
|
||||
case DirectoryType.OneLogin:
|
||||
(configWithSecrets as any).clientSecret = await this.getOneLoginKey();
|
||||
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.setLdapKey(ldapConfig.password);
|
||||
ldapConfig.password = StoredSecurely;
|
||||
await this.setLdapConfiguration(ldapConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.EntraID: {
|
||||
const entraConfig = config as EntraIdConfiguration;
|
||||
await this.setEntraKey(entraConfig.key);
|
||||
entraConfig.key = StoredSecurely;
|
||||
await this.setEntraConfiguration(entraConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.Okta: {
|
||||
const oktaConfig = config as OktaConfiguration;
|
||||
await this.setOktaKey(oktaConfig.token);
|
||||
oktaConfig.token = StoredSecurely;
|
||||
await this.setOktaConfiguration(oktaConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.GSuite: {
|
||||
const gsuiteConfig = config as GSuiteConfiguration;
|
||||
if (gsuiteConfig.privateKey == null) {
|
||||
await this.setGsuiteKey(null);
|
||||
} else {
|
||||
const normalizedPrivateKey = gsuiteConfig.privateKey.replace(/\\n/g, "\n");
|
||||
await this.setGsuiteKey(normalizedPrivateKey);
|
||||
gsuiteConfig.privateKey = StoredSecurely;
|
||||
}
|
||||
await this.setGsuiteConfiguration(gsuiteConfig);
|
||||
break;
|
||||
}
|
||||
case DirectoryType.OneLogin: {
|
||||
const oneLoginConfig = config as OneLoginConfiguration;
|
||||
await this.setOneLoginKey(oneLoginConfig.clientSecret);
|
||||
oneLoginConfig.clientSecret = StoredSecurely;
|
||||
await this.setOneLoginConfiguration(oneLoginConfig);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
async getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directoryConfigurations?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganizationId(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.organizationId;
|
||||
}
|
||||
|
||||
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
|
||||
const currentId = await this.getOrganizationId();
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.directoryType;
|
||||
}
|
||||
|
||||
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
|
||||
const currentType = await this.getDirectoryType();
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getLastSyncHash(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
||||
?.directorySettings?.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));
|
||||
}
|
||||
|
||||
async getUserDelta(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async getGroupDelta(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.directorySettings?.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()),
|
||||
);
|
||||
}
|
||||
|
||||
async clearSyncSettings(hashToo = false) {
|
||||
await this.setUserDelta(null);
|
||||
await this.setGroupDelta(null);
|
||||
await this.setLastGroupSync(null);
|
||||
await this.setLastUserSync(null);
|
||||
if (hashToo) {
|
||||
await this.setLastSyncHash(null);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
||||
return this.getGlobalEnvironmentUrls(options);
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
|
||||
import { StateMigrationService as BaseStateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||
|
||||
import { DirectoryType } from "@/src/enums/directoryType";
|
||||
import { Account, DirectoryConfigurations, DirectorySettings } 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 { SyncConfiguration } from "@/src/models/syncConfiguration";
|
||||
|
||||
const SecureStorageKeys: { [key: string]: any } = {
|
||||
ldap: "ldapPassword",
|
||||
gsuite: "gsuitePrivateKey",
|
||||
azure: "azureKey",
|
||||
entra: "entraIdKey",
|
||||
okta: "oktaToken",
|
||||
oneLogin: "oneLoginClientSecret",
|
||||
directoryConfigPrefix: "directoryConfig_",
|
||||
sync: "syncConfig",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
};
|
||||
|
||||
const Keys: { [key: string]: any } = {
|
||||
entityId: "entityId",
|
||||
directoryType: "directoryType",
|
||||
organizationId: "organizationId",
|
||||
lastUserSync: "lastUserSync",
|
||||
lastGroupSync: "lastGroupSync",
|
||||
lastSyncHash: "lastSyncHash",
|
||||
syncingDir: "syncingDir",
|
||||
syncConfig: "syncConfig",
|
||||
userDelta: "userDeltaToken",
|
||||
groupDelta: "groupDeltaToken",
|
||||
tempDirectoryConfigs: "tempDirectoryConfigs",
|
||||
tempDirectorySettings: "tempDirectorySettings",
|
||||
};
|
||||
|
||||
const StateKeys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
};
|
||||
|
||||
const ClientKeys: { [key: string]: any } = {
|
||||
clientIdOld: "clientId",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecretOld: "clientSecret",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
};
|
||||
|
||||
export class StateMigrationService extends BaseStateMigrationService {
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion = await this.getCurrentStateVersion();
|
||||
while (currentStateVersion < StateVersion.Latest) {
|
||||
switch (currentStateVersion) {
|
||||
case StateVersion.One:
|
||||
await this.migrateClientKeys();
|
||||
await this.migrateStateFrom1To2();
|
||||
break;
|
||||
case StateVersion.Two:
|
||||
await this.migrateStateFrom2To3();
|
||||
}
|
||||
currentStateVersion += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this migration when we are confident existing api keys are all migrated. Probably 1-2 releases.
|
||||
protected async migrateClientKeys() {
|
||||
const oldClientId = await this.storageService.get<string>(ClientKeys.clientIdOld);
|
||||
const oldClientSecret = await this.storageService.get<string>(ClientKeys.clientSecretOld);
|
||||
|
||||
if (oldClientId != null) {
|
||||
await this.storageService.save(ClientKeys.clientId, oldClientId);
|
||||
await this.storageService.remove(ClientKeys.clientIdOld);
|
||||
}
|
||||
|
||||
if (oldClientSecret != null) {
|
||||
await this.storageService.save(ClientKeys.clientSecret, oldClientSecret);
|
||||
await this.storageService.remove(ClientKeys.clientSecretOld);
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrateStateFrom1To2(useSecureStorageForSecrets = true): Promise<void> {
|
||||
// Grabbing a couple of key settings before they get cleared by the base 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
|
||||
const clearDirectoryConnectorV1Keys = async () => {
|
||||
for (const key in Keys) {
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
for (const directoryType in DirectoryType) {
|
||||
if (directoryType == null) {
|
||||
continue;
|
||||
}
|
||||
await this.set(SecureStorageKeys.directoryConfigPrefix + directoryType, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
const getDirectoryConfig = async <T>(type: DirectoryType) =>
|
||||
await this.get<T>(SecureStorageKeys.directoryConfigPrefix + type);
|
||||
const directoryConfigs: DirectoryConfigurations = {
|
||||
ldap: await getDirectoryConfig<LdapConfiguration>(DirectoryType.Ldap),
|
||||
gsuite: await getDirectoryConfig<GSuiteConfiguration>(DirectoryType.GSuite),
|
||||
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
|
||||
// to be backwards compatible with existing configurations.
|
||||
azure: await getDirectoryConfig<EntraIdConfiguration>(DirectoryType.EntraID),
|
||||
entra: await getDirectoryConfig<EntraIdConfiguration>(DirectoryType.EntraID),
|
||||
okta: await getDirectoryConfig<OktaConfiguration>(DirectoryType.Okta),
|
||||
oneLogin: await getDirectoryConfig<OneLoginConfiguration>(DirectoryType.OneLogin),
|
||||
};
|
||||
|
||||
const directorySettings: DirectorySettings = {
|
||||
directoryType: await this.get<DirectoryType>(Keys.directoryType),
|
||||
organizationId: await this.get<string>(Keys.organizationId),
|
||||
lastUserSync: await this.get<Date>(Keys.lastUserSync),
|
||||
lastGroupSync: await this.get<Date>(Keys.lastGroupSync),
|
||||
lastSyncHash: await this.get<string>(Keys.lastSyncHash),
|
||||
syncingDir: await this.get<boolean>(Keys.syncingDir),
|
||||
sync: await this.get<SyncConfiguration>(Keys.syncConfig),
|
||||
userDelta: await this.get<string>(Keys.userDelta),
|
||||
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
|
||||
if (userId == null) {
|
||||
await this.set(Keys.tempDirectoryConfigs, directoryConfigs);
|
||||
await this.set(Keys.tempDirectorySettings, directorySettings);
|
||||
await clearDirectoryConnectorV1Keys();
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.get<Account>(userId);
|
||||
account.directoryConfigurations = directoryConfigs;
|
||||
account.directorySettings = directorySettings;
|
||||
account.profile = {
|
||||
userId: userId,
|
||||
entityId: userId,
|
||||
apiKeyClientId: clientId,
|
||||
};
|
||||
account.clientKeys = {
|
||||
clientId: clientId,
|
||||
clientSecret: clientSecret,
|
||||
};
|
||||
|
||||
await this.set(userId, account);
|
||||
await clearDirectoryConnectorV1Keys();
|
||||
|
||||
if (useSecureStorageForSecrets) {
|
||||
for (const key in SecureStorageKeys) {
|
||||
if (await this.secureStorageService.has(SecureStorageKeys[key])) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}_${SecureStorageKeys[key]}`,
|
||||
await this.secureStorageService.get(SecureStorageKeys[key]),
|
||||
);
|
||||
await this.secureStorageService.remove(SecureStorageKeys[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected async migrateStateFrom2To3(useSecureStorageForSecrets = true): Promise<void> {
|
||||
if (useSecureStorageForSecrets) {
|
||||
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
|
||||
|
||||
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}`);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,17 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||
|
||||
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 { 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";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
@@ -28,7 +27,6 @@ describe("SyncService", () => {
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let directoryFactory: MockProxy<DirectoryFactoryService>;
|
||||
|
||||
let batchRequestBuilder: BatchRequestBuilder;
|
||||
@@ -45,9 +43,9 @@ describe("SyncService", () => {
|
||||
cryptoFunctionService = mock();
|
||||
apiService = mock();
|
||||
messagingService = mock();
|
||||
environmentService = mock();
|
||||
directoryFactory = mock();
|
||||
|
||||
stateService.getApiUrl.mockResolvedValue("https://api.bitwarden.com");
|
||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
|
||||
stateService.getOrganizationId.mockResolvedValue("fakeId");
|
||||
|
||||
@@ -62,7 +60,6 @@ describe("SyncService", () => {
|
||||
apiService,
|
||||
messagingService,
|
||||
i18nService,
|
||||
environmentService,
|
||||
stateService,
|
||||
batchRequestBuilder,
|
||||
singleRequestBuilder,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { OrganizationImportRequest } from "@/jslib/common/src/models/request/organizationImportRequest";
|
||||
import { ApiService } from "@/jslib/common/src/services/api.service";
|
||||
@@ -10,13 +9,13 @@ import { GroupEntry } from "@/src/models/groupEntry";
|
||||
|
||||
import { getSyncConfiguration } from "../../utils/openldap/config-fixtures";
|
||||
import { DirectoryFactoryService } from "../abstractions/directory-factory.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";
|
||||
import { SyncService } from "./sync.service";
|
||||
import * as constants from "./sync.service";
|
||||
|
||||
@@ -28,7 +27,6 @@ describe("SyncService", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let directoryFactory: MockProxy<DirectoryFactoryService>;
|
||||
let batchRequestBuilder: MockProxy<BatchRequestBuilder>;
|
||||
@@ -43,12 +41,12 @@ describe("SyncService", () => {
|
||||
apiService = mock();
|
||||
messagingService = mock();
|
||||
i18nService = mock();
|
||||
environmentService = mock();
|
||||
stateService = mock();
|
||||
directoryFactory = mock();
|
||||
batchRequestBuilder = mock();
|
||||
singleRequestBuilder = mock();
|
||||
|
||||
stateService.getApiUrl.mockResolvedValue("https://api.bitwarden.com");
|
||||
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
|
||||
stateService.getOrganizationId.mockResolvedValue("fakeId");
|
||||
const mockDirectoryService = mock<LdapDirectoryService>();
|
||||
@@ -60,7 +58,6 @@ describe("SyncService", () => {
|
||||
apiService,
|
||||
messagingService,
|
||||
i18nService,
|
||||
environmentService,
|
||||
stateService,
|
||||
batchRequestBuilder,
|
||||
singleRequestBuilder,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
@@ -31,7 +30,6 @@ export class SyncService {
|
||||
private apiService: ApiService,
|
||||
private messagingService: MessagingService,
|
||||
private i18nService: I18nService,
|
||||
private environmentService: EnvironmentService,
|
||||
private stateService: StateService,
|
||||
private batchRequestBuilder: BatchRequestBuilder,
|
||||
private singleRequestBuilder: SingleRequestBuilder,
|
||||
@@ -119,19 +117,14 @@ export class SyncService {
|
||||
}
|
||||
|
||||
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
|
||||
const apiUrl = await this.stateService.getApiUrl();
|
||||
let hashLegacy: string = null;
|
||||
const hashBuffLegacy = await this.cryptoFunctionService.hash(
|
||||
this.environmentService.getApiUrl() + reqJson,
|
||||
"sha256",
|
||||
);
|
||||
const hashBuffLegacy = await this.cryptoFunctionService.hash(apiUrl + reqJson, "sha256");
|
||||
if (hashBuffLegacy != null) {
|
||||
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
|
||||
}
|
||||
let hash: string = null;
|
||||
const hashBuff = await this.cryptoFunctionService.hash(
|
||||
this.environmentService.getApiUrl() + orgId + reqJson,
|
||||
"sha256",
|
||||
);
|
||||
const hashBuff = await this.cryptoFunctionService.hash(apiUrl + orgId + reqJson, "sha256");
|
||||
if (hashBuff != null) {
|
||||
hash = Utils.fromBufferToB64(hashBuff);
|
||||
}
|
||||
|
||||
90
src/services/token/token.service.ts
Normal file
90
src/services/token/token.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
|
||||
import { TokenService as ITokenService } from "@/src/abstractions/token.service";
|
||||
import {
|
||||
DecodedToken,
|
||||
decodeJwt,
|
||||
tokenNeedsRefresh as checkTokenNeedsRefresh,
|
||||
} from "@/src/utils/jwt.util";
|
||||
|
||||
export class TokenService implements ITokenService {
|
||||
// Storage keys
|
||||
private TOKEN_KEY = "accessToken";
|
||||
private REFRESH_TOKEN_KEY = "refreshToken";
|
||||
private CLIENT_ID_KEY = "apiKeyClientId";
|
||||
private CLIENT_SECRET_KEY = "apiKeyClientSecret";
|
||||
private TWO_FACTOR_TOKEN_KEY = "twoFactorToken";
|
||||
|
||||
constructor(private secureStorageService: StorageService) {}
|
||||
|
||||
async setTokens(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret?: [string, string],
|
||||
): Promise<void> {
|
||||
await this.secureStorageService.save(this.TOKEN_KEY, accessToken);
|
||||
await this.secureStorageService.save(this.REFRESH_TOKEN_KEY, refreshToken);
|
||||
|
||||
if (clientIdClientSecret) {
|
||||
await this.secureStorageService.save(this.CLIENT_ID_KEY, clientIdClientSecret[0]);
|
||||
await this.secureStorageService.save(this.CLIENT_SECRET_KEY, clientIdClientSecret[1]);
|
||||
}
|
||||
}
|
||||
|
||||
async getToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TOKEN_KEY);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearToken(): Promise<void> {
|
||||
await this.secureStorageService.remove(this.TOKEN_KEY);
|
||||
await this.secureStorageService.remove(this.REFRESH_TOKEN_KEY);
|
||||
await this.secureStorageService.remove(this.CLIENT_ID_KEY);
|
||||
await this.secureStorageService.remove(this.CLIENT_SECRET_KEY);
|
||||
}
|
||||
|
||||
async getClientId(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_ID_KEY);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.CLIENT_SECRET_KEY);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string | null> {
|
||||
return await this.secureStorageService.get<string>(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<void> {
|
||||
await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async decodeToken(token?: string): Promise<DecodedToken | null> {
|
||||
const tokenToUse = token ?? (await this.getToken());
|
||||
if (!tokenToUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeJwt(tokenToUse);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise<boolean> {
|
||||
const token = await this.getToken();
|
||||
if (!token) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return checkTokenNeedsRefresh(token, minutesBeforeExpiration);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/utils/jwt.util.ts
Normal file
44
src/utils/jwt.util.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface DecodedToken {
|
||||
exp: number;
|
||||
iat: number;
|
||||
nbf: number;
|
||||
sub: string; // user ID
|
||||
client_id?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function decodeJwt(token: string): DecodedToken {
|
||||
// Validate JWT structure (3 parts: header.payload.signature)
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid JWT format");
|
||||
}
|
||||
|
||||
// Decode payload (base64url to JSON)
|
||||
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
return JSON.parse(atob(payload));
|
||||
}
|
||||
|
||||
export function getTokenExpirationDate(token: string): Date | null {
|
||||
const decoded = decodeJwt(token);
|
||||
if (!decoded.exp) {
|
||||
return null;
|
||||
}
|
||||
return new Date(decoded.exp * 1000);
|
||||
}
|
||||
|
||||
export function tokenSecondsRemaining(token: string, offsetSeconds = 0): number {
|
||||
const expDate = getTokenExpirationDate(token);
|
||||
if (!expDate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const msRemaining = expDate.getTime() - Date.now() - offsetSeconds * 1000;
|
||||
return Math.floor(msRemaining / 1000);
|
||||
}
|
||||
|
||||
export function tokenNeedsRefresh(token: string, minutesBeforeExpiration = 5): boolean {
|
||||
const secondsRemaining = tokenSecondsRemaining(token);
|
||||
return secondsRemaining < minutesBeforeExpiration * 60;
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
const path = require("path");
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const webpack = require("webpack");
|
||||
const nodeExternals = require("webpack-node-externals");
|
||||
import CopyWebpackPlugin from "copy-webpack-plugin";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
import webpack from "webpack";
|
||||
import nodeExternals from "webpack-node-externals";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = "development";
|
||||
@@ -69,4 +74,4 @@ const config = {
|
||||
externals: [nodeExternals()],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
export default config;
|
||||
@@ -1,8 +1,14 @@
|
||||
const path = require("path");
|
||||
const { merge } = require("webpack-merge");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const nodeExternals = require("webpack-node-externals");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import { merge } from "webpack-merge";
|
||||
import CopyWebpackPlugin from "copy-webpack-plugin";
|
||||
import nodeExternals from "webpack-node-externals";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const common = {
|
||||
module: {
|
||||
@@ -62,4 +68,4 @@ const main = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = merge(common, main);
|
||||
export default merge(common, main);
|
||||
@@ -1,10 +1,17 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import webpack from "webpack";
|
||||
import { merge } from "webpack-merge";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import { AngularWebpackPlugin } from "@ngtools/webpack";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
import sass from "sass";
|
||||
|
||||
// ESM __dirname polyfill for Node 20
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const common = {
|
||||
module: {
|
||||
@@ -99,7 +106,7 @@ const renderer = {
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
implementation: require("sass"),
|
||||
implementation: sass,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -138,4 +145,4 @@ const renderer = {
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = merge(common, renderer);
|
||||
export default merge(common, renderer);
|
||||
Reference in New Issue
Block a user