mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-18 01:41:27 +01:00
[PM-6819] Credential generator MV3 integration (#8998)
* replace `PasswordGeneratorService` with `legacyPasswordGenerationServiceFactory` * replace `UsernameGeneratorService` with `legacyUsernameGenerationServiceFactory` * migrate generator options and history * apply policy immediately once available * suppress duplicate policy emissions * run password generation response code in `ngZone`
This commit is contained in:
parent
97c7ef3f21
commit
a16dc84a0a
@ -144,13 +144,11 @@ import { NotificationsService } from "@bitwarden/common/services/notifications.s
|
|||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import {
|
import {
|
||||||
PasswordGenerationService,
|
legacyPasswordGenerationServiceFactory,
|
||||||
PasswordGenerationServiceAbstraction,
|
legacyUsernameGenerationServiceFactory,
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
} from "@bitwarden/common/tools/generator";
|
||||||
import {
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
UsernameGenerationService,
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
UsernameGenerationServiceAbstraction,
|
|
||||||
} from "@bitwarden/common/tools/generator/username";
|
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
@ -649,10 +647,12 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.passwordStrengthService = new PasswordStrengthService();
|
this.passwordStrengthService = new PasswordStrengthService();
|
||||||
|
|
||||||
this.passwordGenerationService = new PasswordGenerationService(
|
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||||
|
this.encryptService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.stateService,
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||||
@ -1092,10 +1092,14 @@ export default class MainBackground {
|
|||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.usernameGenerationService = new UsernameGenerationService(
|
this.usernameGenerationService = legacyUsernameGenerationServiceFactory(
|
||||||
this.cryptoService,
|
|
||||||
this.stateService,
|
|
||||||
this.apiService,
|
this.apiService,
|
||||||
|
this.i18nService,
|
||||||
|
this.cryptoService,
|
||||||
|
this.encryptService,
|
||||||
|
this.policyService,
|
||||||
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.popupOnlyContext) {
|
if (!this.popupOnlyContext) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Location } from "@angular/common";
|
import { Location } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -29,22 +28,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
stateService: StateService,
|
accountService: AccountService,
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
accountService: AccountService,
|
ngZone: NgZone,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
usernameGenerationService,
|
usernameGenerationService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
stateService,
|
accountService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
route,
|
route,
|
||||||
accountService,
|
ngZone,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
this.cipherService = cipherService;
|
this.cipherService = cipherService;
|
||||||
|
@ -103,10 +103,8 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa
|
|||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||||
import {
|
import { legacyPasswordGenerationServiceFactory } from "@bitwarden/common/tools/generator";
|
||||||
PasswordGenerationService,
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
PasswordGenerationServiceAbstraction,
|
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
@ -499,10 +497,12 @@ export class ServiceContainer {
|
|||||||
|
|
||||||
this.passwordStrengthService = new PasswordStrengthService();
|
this.passwordStrengthService = new PasswordStrengthService();
|
||||||
|
|
||||||
this.passwordGenerationService = new PasswordGenerationService(
|
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||||
|
this.encryptService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.stateService,
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||||
|
@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -36,10 +35,6 @@ describe("GeneratorComponent", () => {
|
|||||||
provide: UsernameGenerationServiceAbstraction,
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: StateService,
|
|
||||||
useValue: mock<StateService>(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
useValue: platformUtilsServiceMock,
|
useValue: platformUtilsServiceMock,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||||
@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
|
|
||||||
@ -18,22 +17,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
stateService: StateService,
|
accountService: AccountService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
|
ngZone: NgZone,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
accountService: AccountService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
usernameGenerationService,
|
usernameGenerationService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
stateService,
|
accountService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
route,
|
route,
|
||||||
accountService,
|
ngZone,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||||
@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@ -21,23 +20,23 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
stateService: StateService,
|
accountService: AccountService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
|
ngZone: NgZone,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
accountService: AccountService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
usernameGenerationService,
|
usernameGenerationService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
stateService,
|
accountService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
route,
|
route,
|
||||||
accountService,
|
ngZone,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
if (platformUtilsService.isSelfHost()) {
|
if (platformUtilsService.isSelfHost()) {
|
||||||
|
@ -193,13 +193,11 @@ import { SearchService } from "@bitwarden/common/services/search.service";
|
|||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||||
import {
|
import {
|
||||||
PasswordGenerationService,
|
legacyPasswordGenerationServiceFactory,
|
||||||
PasswordGenerationServiceAbstraction,
|
legacyUsernameGenerationServiceFactory,
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
} from "@bitwarden/common/tools/generator";
|
||||||
import {
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
UsernameGenerationService,
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
UsernameGenerationServiceAbstraction,
|
|
||||||
} from "@bitwarden/common/tools/generator/username";
|
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
@ -559,13 +557,27 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: PasswordGenerationServiceAbstraction,
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
useClass: PasswordGenerationService,
|
useFactory: legacyPasswordGenerationServiceFactory,
|
||||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
deps: [
|
||||||
|
EncryptService,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
PolicyServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: UsernameGenerationServiceAbstraction,
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
useClass: UsernameGenerationService,
|
useFactory: legacyUsernameGenerationServiceFactory,
|
||||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
|
deps: [
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
EncryptService,
|
||||||
|
PolicyServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ApiServiceAbstraction,
|
provide: ApiServiceAbstraction,
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
|
||||||
import { debounceTime, first, map } from "rxjs/operators";
|
import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { GeneratorType } from "@bitwarden/common/tools/generator/generator-type";
|
||||||
import { GeneratorOptions } from "@bitwarden/common/tools/generator/generator-options";
|
|
||||||
import {
|
import {
|
||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
PasswordGeneratorOptions,
|
PasswordGeneratorOptions,
|
||||||
@ -22,9 +21,9 @@ import {
|
|||||||
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class GeneratorComponent implements OnInit {
|
export class GeneratorComponent implements OnInit, OnDestroy {
|
||||||
@Input() comingFromAddEdit = false;
|
@Input() comingFromAddEdit = false;
|
||||||
@Input() type: string;
|
@Input() type: GeneratorType | "";
|
||||||
@Output() onSelected = new EventEmitter<string>();
|
@Output() onSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
usernameGeneratingPromise: Promise<string>;
|
usernameGeneratingPromise: Promise<string>;
|
||||||
@ -43,6 +42,9 @@ export class GeneratorComponent implements OnInit {
|
|||||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||||
usernameWebsite: string = null;
|
usernameWebsite: string = null;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private isInitialized$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
// update screen reader minimum password length with 500ms debounce
|
// update screen reader minimum password length with 500ms debounce
|
||||||
// so that the user isn't flooded with status updates
|
// so that the user isn't flooded with status updates
|
||||||
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
||||||
@ -53,15 +55,17 @@ export class GeneratorComponent implements OnInit {
|
|||||||
debounceTime(500),
|
debounceTime(500),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _password = new BehaviorSubject<string>("-");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
protected stateService: StateService,
|
protected accountService: AccountService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected accountService: AccountService,
|
protected ngZone: NgZone,
|
||||||
private win: Window,
|
private win: Window,
|
||||||
) {
|
) {
|
||||||
this.typeOptions = [
|
this.typeOptions = [
|
||||||
@ -92,22 +96,41 @@ export class GeneratorComponent implements OnInit {
|
|||||||
];
|
];
|
||||||
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
this.forwardOptions = [
|
||||||
this.initForwardOptions();
|
{ name: "", value: "", validForSelfHosted: false },
|
||||||
|
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||||
|
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||||
|
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||||
|
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||||
|
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||||
|
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||||
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
this._password.pipe(debounceTime(250)).subscribe((password) => {
|
||||||
|
ngZone.run(() => {
|
||||||
|
this.password = password;
|
||||||
|
});
|
||||||
|
this.passwordGenerationService.addHistory(this.password).catch((e) => {
|
||||||
|
this.logService.error(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
|
||||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
|
||||||
const passwordOptionsResponse = await this.passwordGenerationService.getOptions();
|
|
||||||
this.passwordOptions = passwordOptionsResponse[0];
|
|
||||||
this.enforcedPasswordPolicyOptions = passwordOptionsResponse[1];
|
|
||||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||||
|
|
||||||
|
if (!this.type) {
|
||||||
|
if (navigationType) {
|
||||||
|
this.type = navigationType;
|
||||||
|
} else {
|
||||||
|
this.type = this.passwordOptions.type === "username" ? "username" : "password";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.passwordOptions.type =
|
this.passwordOptions.type =
|
||||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||||
|
|
||||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
|
||||||
if (this.usernameOptions.type == null) {
|
if (this.usernameOptions.type == null) {
|
||||||
this.usernameOptions.type = "word";
|
this.usernameOptions.type = "word";
|
||||||
}
|
}
|
||||||
@ -115,9 +138,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
this.usernameOptions.subaddressEmail == null ||
|
this.usernameOptions.subaddressEmail == null ||
|
||||||
this.usernameOptions.subaddressEmail === ""
|
this.usernameOptions.subaddressEmail === ""
|
||||||
) {
|
) {
|
||||||
this.usernameOptions.subaddressEmail = await firstValueFrom(
|
this.usernameOptions.subaddressEmail = accountEmail;
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (this.usernameWebsite == null) {
|
if (this.usernameWebsite == null) {
|
||||||
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||||
@ -127,26 +148,63 @@ export class GeneratorComponent implements OnInit {
|
|||||||
this.subaddressOptions.push(websiteOption);
|
this.subaddressOptions.push(websiteOption);
|
||||||
this.catchallOptions.push(websiteOption);
|
this.catchallOptions.push(websiteOption);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
combineLatest([
|
||||||
|
this.route.queryParams.pipe(first()),
|
||||||
|
this.accountService.activeAccount$.pipe(first()),
|
||||||
|
this.passwordGenerationService.getOptions$(),
|
||||||
|
this.usernameGenerationService.getOptions$(),
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({
|
||||||
|
navigationType: qParams.type as GeneratorType,
|
||||||
|
accountEmail: account.email,
|
||||||
|
passwordOptions,
|
||||||
|
passwordPolicy,
|
||||||
|
usernameOptions,
|
||||||
|
})),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((options) => {
|
||||||
|
this.passwordOptions = options.passwordOptions;
|
||||||
|
this.enforcedPasswordPolicyOptions = options.passwordPolicy;
|
||||||
|
this.usernameOptions = options.usernameOptions;
|
||||||
|
|
||||||
|
this.cascadeOptions(options.navigationType, options.accountEmail);
|
||||||
|
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||||
|
|
||||||
if (this.type !== "username" && this.type !== "password") {
|
|
||||||
if (qParams.type === "username" || qParams.type === "password") {
|
|
||||||
this.type = qParams.type;
|
|
||||||
} else {
|
|
||||||
const generatorOptions = await this.stateService.getGeneratorOptions();
|
|
||||||
this.type = generatorOptions?.type ?? "password";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.regenerateWithoutButtonPress()) {
|
if (this.regenerateWithoutButtonPress()) {
|
||||||
await this.regenerate();
|
this.regenerate().catch((e) => {
|
||||||
}
|
this.logService.error(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async typeChanged() {
|
this.isInitialized$.next(true);
|
||||||
await this.stateService.setGeneratorOptions({ type: this.type } as GeneratorOptions);
|
});
|
||||||
if (this.regenerateWithoutButtonPress()) {
|
|
||||||
await this.regenerate();
|
// once initialization is complete, `ngOnInit` should return.
|
||||||
|
//
|
||||||
|
// FIXME(#6944): if a sync is in progress, wait to complete until after
|
||||||
|
// the sync completes.
|
||||||
|
await firstValueFrom(
|
||||||
|
this.isInitialized$.pipe(
|
||||||
|
skipWhile((initialized) => !initialized),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
this.isInitialized$.complete();
|
||||||
|
this._passwordOptionsMinLengthForReader.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeChanged() {
|
||||||
|
await this.savePasswordOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async regenerate() {
|
async regenerate() {
|
||||||
@ -160,7 +218,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
async sliderChanged() {
|
async sliderChanged() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.savePasswordOptions(false);
|
this.savePasswordOptions();
|
||||||
await this.passwordGenerationService.addHistory(this.password);
|
await this.passwordGenerationService.addHistory(this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,31 +262,34 @@ export class GeneratorComponent implements OnInit {
|
|||||||
|
|
||||||
async sliderInput() {
|
async sliderInput() {
|
||||||
await this.normalizePasswordOptions();
|
await this.normalizePasswordOptions();
|
||||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePasswordOptions(regenerate = true) {
|
async savePasswordOptions() {
|
||||||
|
// map navigation state into generator type
|
||||||
|
const restoreType = this.passwordOptions.type;
|
||||||
|
if (this.type === "username") {
|
||||||
|
this.passwordOptions.type = this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save options
|
||||||
await this.normalizePasswordOptions();
|
await this.normalizePasswordOptions();
|
||||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||||
|
|
||||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
// restore the original format
|
||||||
await this.regeneratePassword();
|
this.passwordOptions.type = restoreType;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveUsernameOptions(regenerate = true) {
|
async saveUsernameOptions() {
|
||||||
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||||
if (this.usernameOptions.type === "forwarded") {
|
if (this.usernameOptions.type === "forwarded") {
|
||||||
this.username = "-";
|
this.username = "-";
|
||||||
}
|
}
|
||||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
|
||||||
await this.regenerateUsername();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async regeneratePassword() {
|
async regeneratePassword() {
|
||||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
this._password.next(
|
||||||
await this.passwordGenerationService.addHistory(this.password);
|
await this.passwordGenerationService.generatePassword(this.passwordOptions),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
regenerateUsername() {
|
regenerateUsername() {
|
||||||
@ -297,28 +358,5 @@ export class GeneratorComponent implements OnInit {
|
|||||||
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
||||||
this.passwordOptions,
|
this.passwordOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initForwardOptions() {
|
|
||||||
this.forwardOptions = [
|
|
||||||
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
|
||||||
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
|
||||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
|
||||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
|
||||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
|
||||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
|
||||||
if (
|
|
||||||
this.usernameOptions.forwardedService == null ||
|
|
||||||
this.usernameOptions.forwardedService === ""
|
|
||||||
) {
|
|
||||||
this.forwardOptions.push({ name: "", value: null, validForSelfHosted: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.forwardOptions = this.forwardOptions.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear = async () => {
|
clear = async () => {
|
||||||
this.history = [];
|
this.history = await this.passwordGenerationService.clear();
|
||||||
await this.passwordGenerationService.clear();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
copy(password: string) {
|
copy(password: string) {
|
||||||
|
@ -70,7 +70,7 @@ export class PasswordGeneratorPolicyOptions extends Domain {
|
|||||||
*/
|
*/
|
||||||
inEffect() {
|
inEffect() {
|
||||||
return (
|
return (
|
||||||
this.defaultType !== "" ||
|
this.defaultType ||
|
||||||
this.minLength > 0 ||
|
this.minLength > 0 ||
|
||||||
this.numberCount > 0 ||
|
this.numberCount > 0 ||
|
||||||
this.specialCount > 0 ||
|
this.specialCount > 0 ||
|
||||||
|
@ -60,13 +60,16 @@ import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key
|
|||||||
import { KnownAccountsMigrator } from "./migrations/60-known-accounts";
|
import { KnownAccountsMigrator } from "./migrations/60-known-accounts";
|
||||||
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
||||||
import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider";
|
import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider";
|
||||||
|
import { PasswordOptionsMigrator } from "./migrations/63-migrate-password-settings";
|
||||||
|
import { GeneratorHistoryMigrator } from "./migrations/64-migrate-generator-history";
|
||||||
|
import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-settings";
|
||||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||||
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 62;
|
export const CURRENT_VERSION = 65;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@ -130,7 +133,10 @@ export function createMigrationBuilder() {
|
|||||||
.with(KdfConfigMigrator, 58, 59)
|
.with(KdfConfigMigrator, 58, 59)
|
||||||
.with(KnownAccountsMigrator, 59, 60)
|
.with(KnownAccountsMigrator, 59, 60)
|
||||||
.with(PinStateMigrator, 60, 61)
|
.with(PinStateMigrator, 60, 61)
|
||||||
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, CURRENT_VERSION);
|
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, 62)
|
||||||
|
.with(PasswordOptionsMigrator, 62, 63)
|
||||||
|
.with(GeneratorHistoryMigrator, 63, 64)
|
||||||
|
.with(ForwarderOptionsMigrator, 64, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExpectedOptions,
|
||||||
|
PasswordOptionsMigrator,
|
||||||
|
NAVIGATION,
|
||||||
|
PASSWORD,
|
||||||
|
PASSPHRASE,
|
||||||
|
} from "./63-migrate-password-settings";
|
||||||
|
|
||||||
|
function migrationHelper(passwordGenerationOptions: ExpectedOptions) {
|
||||||
|
const helper = mockMigrationHelper(
|
||||||
|
{
|
||||||
|
global_account_accounts: {
|
||||||
|
SomeAccount: {
|
||||||
|
email: "SomeAccount",
|
||||||
|
name: "SomeAccount",
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SomeAccount: {
|
||||||
|
settings: {
|
||||||
|
passwordGenerationOptions,
|
||||||
|
this: {
|
||||||
|
looks: "important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cant: {
|
||||||
|
touch: "this",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
62,
|
||||||
|
);
|
||||||
|
|
||||||
|
return helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||||
|
settings: {
|
||||||
|
this: {
|
||||||
|
looks: "important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cant: {
|
||||||
|
touch: "this",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PasswordOptionsMigrator", () => {
|
||||||
|
describe("migrate", () => {
|
||||||
|
it("migrates generator type", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
type: "password",
|
||||||
|
});
|
||||||
|
helper.getFromUser.mockResolvedValue({ some: { other: "data" } });
|
||||||
|
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", NAVIGATION, {
|
||||||
|
type: "password",
|
||||||
|
some: { other: "data" },
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates password settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
length: 20,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: false,
|
||||||
|
minUppercase: 4,
|
||||||
|
lowercase: true,
|
||||||
|
minLowercase: 3,
|
||||||
|
number: false,
|
||||||
|
minNumber: 2,
|
||||||
|
special: true,
|
||||||
|
minSpecial: 1,
|
||||||
|
});
|
||||||
|
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", PASSWORD, {
|
||||||
|
length: 20,
|
||||||
|
ambiguous: true,
|
||||||
|
uppercase: false,
|
||||||
|
minUppercase: 4,
|
||||||
|
lowercase: true,
|
||||||
|
minLowercase: 3,
|
||||||
|
number: false,
|
||||||
|
minNumber: 2,
|
||||||
|
special: true,
|
||||||
|
minSpecial: 1,
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates passphrase settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
numWords: 5,
|
||||||
|
wordSeparator: "4",
|
||||||
|
capitalize: true,
|
||||||
|
includeNumber: false,
|
||||||
|
});
|
||||||
|
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", PASSPHRASE, {
|
||||||
|
numWords: 5,
|
||||||
|
wordSeparator: "4",
|
||||||
|
capitalize: true,
|
||||||
|
includeNumber: false,
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,150 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
/** settings targeted by migrator */
|
||||||
|
export type AccountType = {
|
||||||
|
settings?: {
|
||||||
|
passwordGenerationOptions?: ExpectedOptions;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GeneratorType = "password" | "passphrase" | "username";
|
||||||
|
|
||||||
|
/** username generation options prior to refactoring */
|
||||||
|
export type ExpectedOptions = {
|
||||||
|
type?: GeneratorType;
|
||||||
|
length?: number;
|
||||||
|
minLength?: number;
|
||||||
|
ambiguous?: boolean;
|
||||||
|
uppercase?: boolean;
|
||||||
|
minUppercase?: number;
|
||||||
|
lowercase?: boolean;
|
||||||
|
minLowercase?: number;
|
||||||
|
number?: boolean;
|
||||||
|
minNumber?: number;
|
||||||
|
special?: boolean;
|
||||||
|
minSpecial?: number;
|
||||||
|
numWords?: number;
|
||||||
|
wordSeparator?: string;
|
||||||
|
capitalize?: boolean;
|
||||||
|
includeNumber?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** username generation options after refactoring */
|
||||||
|
type ConvertedOptions = {
|
||||||
|
generator: GeneratorNavigation;
|
||||||
|
password: PasswordGenerationOptions;
|
||||||
|
passphrase: PassphraseGenerationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NAVIGATION: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "generatorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PASSWORD: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "passwordGeneratorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PASSPHRASE: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "passphraseGeneratorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GeneratorNavigation = {
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PassphraseGenerationOptions = {
|
||||||
|
numWords?: number;
|
||||||
|
wordSeparator?: string;
|
||||||
|
capitalize?: boolean;
|
||||||
|
includeNumber?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PasswordGenerationOptions = {
|
||||||
|
length?: number;
|
||||||
|
minLength?: number;
|
||||||
|
ambiguous?: boolean;
|
||||||
|
uppercase?: boolean;
|
||||||
|
minUppercase?: number;
|
||||||
|
lowercase?: boolean;
|
||||||
|
minLowercase?: number;
|
||||||
|
number?: boolean;
|
||||||
|
minNumber?: number;
|
||||||
|
special?: boolean;
|
||||||
|
minSpecial?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PasswordOptionsMigrator extends Migrator<62, 63> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<AccountType>();
|
||||||
|
|
||||||
|
async function migrateAccount(userId: string, account: AccountType) {
|
||||||
|
const legacyOptions = account?.settings?.passwordGenerationOptions;
|
||||||
|
|
||||||
|
if (legacyOptions) {
|
||||||
|
const converted = convertSettings(legacyOptions);
|
||||||
|
await storeSettings(helper, userId, converted);
|
||||||
|
await deleteSettings(helper, userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSettings(options: ExpectedOptions): ConvertedOptions {
|
||||||
|
const password = {
|
||||||
|
length: options.length,
|
||||||
|
ambiguous: options.ambiguous,
|
||||||
|
uppercase: options.uppercase,
|
||||||
|
minUppercase: options.minUppercase,
|
||||||
|
lowercase: options.lowercase,
|
||||||
|
minLowercase: options.minLowercase,
|
||||||
|
number: options.number,
|
||||||
|
minNumber: options.minNumber,
|
||||||
|
special: options.special,
|
||||||
|
minSpecial: options.minSpecial,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = {
|
||||||
|
type: options.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
const passphrase = {
|
||||||
|
numWords: options.numWords,
|
||||||
|
wordSeparator: options.wordSeparator,
|
||||||
|
capitalize: options.capitalize,
|
||||||
|
includeNumber: options.includeNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { generator, password, passphrase };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storeSettings(helper: MigrationHelper, userId: string, converted: ConvertedOptions) {
|
||||||
|
const existing = (await helper.getFromUser(userId, NAVIGATION)) ?? {};
|
||||||
|
const updated = Object.assign(existing, converted.generator);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
helper.setToUser(userId, NAVIGATION, updated),
|
||||||
|
helper.setToUser(userId, PASSPHRASE, converted.passphrase),
|
||||||
|
helper.setToUser(userId, PASSWORD, converted.password),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSettings(helper: MigrationHelper, userId: string, account: AccountType) {
|
||||||
|
delete account?.settings?.passwordGenerationOptions;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EncryptedHistory,
|
||||||
|
GeneratorHistoryMigrator,
|
||||||
|
HISTORY,
|
||||||
|
} from "./64-migrate-generator-history";
|
||||||
|
|
||||||
|
function migrationHelper(encrypted: EncryptedHistory) {
|
||||||
|
const helper = mockMigrationHelper(
|
||||||
|
{
|
||||||
|
global_account_accounts: {
|
||||||
|
SomeAccount: {
|
||||||
|
email: "SomeAccount",
|
||||||
|
name: "SomeAccount",
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SomeAccount: {
|
||||||
|
data: {
|
||||||
|
passwordGenerationHistory: {
|
||||||
|
encrypted,
|
||||||
|
},
|
||||||
|
this: {
|
||||||
|
looks: "important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cant: {
|
||||||
|
touch: "this",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
63,
|
||||||
|
);
|
||||||
|
|
||||||
|
return helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||||
|
data: {
|
||||||
|
this: {
|
||||||
|
looks: "important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cant: {
|
||||||
|
touch: "this",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PasswordOptionsMigrator", () => {
|
||||||
|
describe("migrate", () => {
|
||||||
|
it("migrates generator type", async () => {
|
||||||
|
const helper = migrationHelper([{ this: "should be copied" }, { this: "too" }]);
|
||||||
|
const migrator = new GeneratorHistoryMigrator(63, 64);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", HISTORY, [
|
||||||
|
{ this: "should be copied" },
|
||||||
|
{ this: "too" },
|
||||||
|
]);
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,42 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
/** settings targeted by migrator */
|
||||||
|
export type AccountType = {
|
||||||
|
data?: {
|
||||||
|
passwordGenerationHistory?: {
|
||||||
|
encrypted: EncryptedHistory;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** the actual data stored in the history is opaque to the migrator */
|
||||||
|
export type EncryptedHistory = Array<unknown>;
|
||||||
|
|
||||||
|
export const HISTORY: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "localGeneratorHistoryBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GeneratorHistoryMigrator extends Migrator<63, 64> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<AccountType>();
|
||||||
|
|
||||||
|
async function migrateAccount(userId: string, account: AccountType) {
|
||||||
|
const data = account?.data?.passwordGenerationHistory;
|
||||||
|
if (data && data.encrypted) {
|
||||||
|
await helper.setToUser(userId, HISTORY, data.encrypted);
|
||||||
|
delete account.data.passwordGenerationHistory;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,218 @@
|
|||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ADDY_IO,
|
||||||
|
CATCHALL,
|
||||||
|
DUCK_DUCK_GO,
|
||||||
|
EFF_USERNAME,
|
||||||
|
ExpectedOptions,
|
||||||
|
FASTMAIL,
|
||||||
|
FIREFOX_RELAY,
|
||||||
|
FORWARD_EMAIL,
|
||||||
|
ForwarderOptionsMigrator,
|
||||||
|
NAVIGATION,
|
||||||
|
SIMPLE_LOGIN,
|
||||||
|
SUBADDRESS,
|
||||||
|
} from "./65-migrate-forwarder-settings";
|
||||||
|
|
||||||
|
function migrationHelper(usernameGenerationOptions: ExpectedOptions) {
|
||||||
|
const helper = mockMigrationHelper(
|
||||||
|
{
|
||||||
|
global_account_accounts: {
|
||||||
|
SomeAccount: {
|
||||||
|
email: "SomeAccount",
|
||||||
|
name: "SomeAccount",
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SomeAccount: {
|
||||||
|
settings: {
|
||||||
|
usernameGenerationOptions,
|
||||||
|
this: {
|
||||||
|
looks: "important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cant: {
|
||||||
|
touch: "this",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
64,
|
||||||
|
);
|
||||||
|
|
||||||
|
return helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||||
|
settings: {
|
||||||
|
this: {
|
||||||
|
looks: "important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cant: {
|
||||||
|
touch: "this",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ForwarderOptionsMigrator", () => {
|
||||||
|
describe("migrate", () => {
|
||||||
|
it("migrates generator settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
type: "catchall",
|
||||||
|
forwardedService: "simplelogin",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", NAVIGATION, {
|
||||||
|
username: "catchall",
|
||||||
|
forwarder: "simplelogin",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates catchall settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
catchallType: "random",
|
||||||
|
catchallDomain: "example.com",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", CATCHALL, {
|
||||||
|
catchallType: "random",
|
||||||
|
catchallDomain: "example.com",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates EFF username settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
wordCapitalize: true,
|
||||||
|
wordIncludeNumber: false,
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", EFF_USERNAME, {
|
||||||
|
wordCapitalize: true,
|
||||||
|
wordIncludeNumber: false,
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates subaddress settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
subaddressType: "random",
|
||||||
|
subaddressEmail: "j.d@example.com",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", SUBADDRESS, {
|
||||||
|
subaddressType: "random",
|
||||||
|
subaddressEmail: "j.d@example.com",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates addyIo settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
forwardedAnonAddyBaseUrl: "some_addyio_base",
|
||||||
|
forwardedAnonAddyApiToken: "some_addyio_token",
|
||||||
|
forwardedAnonAddyDomain: "some_addyio_domain",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", ADDY_IO, {
|
||||||
|
baseUrl: "some_addyio_base",
|
||||||
|
token: "some_addyio_token",
|
||||||
|
domain: "some_addyio_domain",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates DuckDuckGo settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
forwardedDuckDuckGoToken: "some_duckduckgo_token",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", DUCK_DUCK_GO, {
|
||||||
|
token: "some_duckduckgo_token",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates Firefox Relay settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
forwardedFirefoxApiToken: "some_firefox_token",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FIREFOX_RELAY, {
|
||||||
|
token: "some_firefox_token",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates Fastmail settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
forwardedFastmailApiToken: "some_fastmail_token",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FASTMAIL, {
|
||||||
|
token: "some_fastmail_token",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates ForwardEmail settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
forwardedForwardEmailApiToken: "some_forwardemail_token",
|
||||||
|
forwardedForwardEmailDomain: "some_forwardemail_domain",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FORWARD_EMAIL, {
|
||||||
|
token: "some_forwardemail_token",
|
||||||
|
domain: "some_forwardemail_domain",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates SimpleLogin settings", async () => {
|
||||||
|
const helper = migrationHelper({
|
||||||
|
forwardedSimpleLoginApiKey: "some_simplelogin_token",
|
||||||
|
forwardedSimpleLoginBaseUrl: "some_simplelogin_baseurl",
|
||||||
|
});
|
||||||
|
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||||
|
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", SIMPLE_LOGIN, {
|
||||||
|
token: "some_simplelogin_token",
|
||||||
|
baseUrl: "some_simplelogin_baseurl",
|
||||||
|
});
|
||||||
|
expectOtherSettingsRemain(helper);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,245 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
/** settings targeted by migrator */
|
||||||
|
export type AccountType = {
|
||||||
|
settings?: {
|
||||||
|
usernameGenerationOptions?: ExpectedOptions;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** username generation options prior to refactoring */
|
||||||
|
export type ExpectedOptions = {
|
||||||
|
type?: "word" | "subaddress" | "catchall" | "forwarded";
|
||||||
|
wordCapitalize?: boolean;
|
||||||
|
wordIncludeNumber?: boolean;
|
||||||
|
subaddressType?: "random" | "website-name";
|
||||||
|
subaddressEmail?: string;
|
||||||
|
catchallType?: "random" | "website-name";
|
||||||
|
catchallDomain?: string;
|
||||||
|
forwardedService?: string;
|
||||||
|
forwardedAnonAddyApiToken?: string;
|
||||||
|
forwardedAnonAddyDomain?: string;
|
||||||
|
forwardedAnonAddyBaseUrl?: string;
|
||||||
|
forwardedDuckDuckGoToken?: string;
|
||||||
|
forwardedFirefoxApiToken?: string;
|
||||||
|
forwardedFastmailApiToken?: string;
|
||||||
|
forwardedForwardEmailApiToken?: string;
|
||||||
|
forwardedForwardEmailDomain?: string;
|
||||||
|
forwardedSimpleLoginApiKey?: string;
|
||||||
|
forwardedSimpleLoginBaseUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** username generation options after refactoring */
|
||||||
|
type ConvertedOptions = {
|
||||||
|
generator: GeneratorNavigation;
|
||||||
|
algorithms: {
|
||||||
|
catchall: CatchallGenerationOptions;
|
||||||
|
effUsername: EffUsernameGenerationOptions;
|
||||||
|
subaddress: SubaddressGenerationOptions;
|
||||||
|
};
|
||||||
|
forwarders: {
|
||||||
|
addyIo: SelfHostedApiOptions & EmailDomainOptions;
|
||||||
|
duckDuckGo: ApiOptions;
|
||||||
|
fastmail: ApiOptions;
|
||||||
|
firefoxRelay: ApiOptions;
|
||||||
|
forwardEmail: ApiOptions & EmailDomainOptions;
|
||||||
|
simpleLogin: SelfHostedApiOptions;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NAVIGATION: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "generatorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CATCHALL: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "catchallGeneratorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EFF_USERNAME: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "effUsernameGeneratorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SUBADDRESS: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "subaddressGeneratorSettings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ADDY_IO: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "addyIoBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DUCK_DUCK_GO: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "duckDuckGoBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FASTMAIL: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "fastmailBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FIREFOX_RELAY: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "firefoxRelayBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FORWARD_EMAIL: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "forwardEmailBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SIMPLE_LOGIN: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "generator",
|
||||||
|
},
|
||||||
|
key: "simpleLoginBuffer",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GeneratorNavigation = {
|
||||||
|
type?: string;
|
||||||
|
username?: string;
|
||||||
|
forwarder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UsernameGenerationMode = "random" | "website-name";
|
||||||
|
|
||||||
|
type CatchallGenerationOptions = {
|
||||||
|
catchallType?: UsernameGenerationMode;
|
||||||
|
catchallDomain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EffUsernameGenerationOptions = {
|
||||||
|
wordCapitalize?: boolean;
|
||||||
|
wordIncludeNumber?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubaddressGenerationOptions = {
|
||||||
|
subaddressType?: UsernameGenerationMode;
|
||||||
|
subaddressEmail?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiOptions = {
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelfHostedApiOptions = ApiOptions & {
|
||||||
|
baseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmailDomainOptions = {
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ForwarderOptionsMigrator extends Migrator<64, 65> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<AccountType>();
|
||||||
|
|
||||||
|
async function migrateAccount(userId: string, account: AccountType) {
|
||||||
|
const legacyOptions = account?.settings?.usernameGenerationOptions;
|
||||||
|
|
||||||
|
if (legacyOptions) {
|
||||||
|
const converted = convertSettings(legacyOptions);
|
||||||
|
await storeSettings(helper, userId, converted);
|
||||||
|
await deleteSettings(helper, userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSettings(options: ExpectedOptions): ConvertedOptions {
|
||||||
|
const forwarders = {
|
||||||
|
addyIo: {
|
||||||
|
baseUrl: options.forwardedAnonAddyBaseUrl,
|
||||||
|
token: options.forwardedAnonAddyApiToken,
|
||||||
|
domain: options.forwardedAnonAddyDomain,
|
||||||
|
},
|
||||||
|
duckDuckGo: {
|
||||||
|
token: options.forwardedDuckDuckGoToken,
|
||||||
|
},
|
||||||
|
fastmail: {
|
||||||
|
token: options.forwardedFastmailApiToken,
|
||||||
|
},
|
||||||
|
firefoxRelay: {
|
||||||
|
token: options.forwardedFirefoxApiToken,
|
||||||
|
},
|
||||||
|
forwardEmail: {
|
||||||
|
token: options.forwardedForwardEmailApiToken,
|
||||||
|
domain: options.forwardedForwardEmailDomain,
|
||||||
|
},
|
||||||
|
simpleLogin: {
|
||||||
|
token: options.forwardedSimpleLoginApiKey,
|
||||||
|
baseUrl: options.forwardedSimpleLoginBaseUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = {
|
||||||
|
username: options.type,
|
||||||
|
forwarder: options.forwardedService,
|
||||||
|
};
|
||||||
|
|
||||||
|
const algorithms = {
|
||||||
|
effUsername: {
|
||||||
|
wordCapitalize: options.wordCapitalize,
|
||||||
|
wordIncludeNumber: options.wordIncludeNumber,
|
||||||
|
},
|
||||||
|
subaddress: {
|
||||||
|
subaddressType: options.subaddressType,
|
||||||
|
subaddressEmail: options.subaddressEmail,
|
||||||
|
},
|
||||||
|
catchall: {
|
||||||
|
catchallType: options.catchallType,
|
||||||
|
catchallDomain: options.catchallDomain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { generator, algorithms, forwarders };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storeSettings(helper: MigrationHelper, userId: string, converted: ConvertedOptions) {
|
||||||
|
await Promise.all([
|
||||||
|
helper.setToUser(userId, NAVIGATION, converted.generator),
|
||||||
|
helper.setToUser(userId, CATCHALL, converted.algorithms.catchall),
|
||||||
|
helper.setToUser(userId, EFF_USERNAME, converted.algorithms.effUsername),
|
||||||
|
helper.setToUser(userId, SUBADDRESS, converted.algorithms.subaddress),
|
||||||
|
helper.setToUser(userId, ADDY_IO, converted.forwarders.addyIo),
|
||||||
|
helper.setToUser(userId, DUCK_DUCK_GO, converted.forwarders.duckDuckGo),
|
||||||
|
helper.setToUser(userId, FASTMAIL, converted.forwarders.fastmail),
|
||||||
|
helper.setToUser(userId, FIREFOX_RELAY, converted.forwarders.firefoxRelay),
|
||||||
|
helper.setToUser(userId, FORWARD_EMAIL, converted.forwarders.forwardEmail),
|
||||||
|
helper.setToUser(userId, SIMPLE_LOGIN, converted.forwarders.simpleLogin),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSettings(helper: MigrationHelper, userId: string, account: AccountType) {
|
||||||
|
delete account?.settings?.usernameGenerationOptions;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
@ -38,6 +38,12 @@ export abstract class GeneratorHistoryService {
|
|||||||
*/
|
*/
|
||||||
take: (userId: UserId, credential: string) => Promise<GeneratedCredential | null>;
|
take: (userId: UserId, credential: string) => Promise<GeneratedCredential | null>;
|
||||||
|
|
||||||
|
/** Deletes a user's credential history.
|
||||||
|
* @param userId identifies the user taking the credential.
|
||||||
|
* @returns A promise that completes when the history is cleared.
|
||||||
|
*/
|
||||||
|
clear: (userId: UserId) => Promise<GeneratedCredential[]>;
|
||||||
|
|
||||||
/** Lists all credentials for a user.
|
/** Lists all credentials for a user.
|
||||||
* @param userId identifies the user listing the credential.
|
* @param userId identifies the user listing the credential.
|
||||||
* @remarks This field is eventually consistent with `track` and `take` operations.
|
* @remarks This field is eventually consistent with `track` and `take` operations.
|
||||||
|
@ -23,9 +23,6 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
|||||||
/** Identifies the policy enforced by the generator. */
|
/** Identifies the policy enforced by the generator. */
|
||||||
policy: PolicyType;
|
policy: PolicyType;
|
||||||
|
|
||||||
/** Length of time in milliseconds to cache the evaluator */
|
|
||||||
cache_ms: number;
|
|
||||||
|
|
||||||
/** Operator function that converts a policy collection observable to a single
|
/** Operator function that converts a policy collection observable to a single
|
||||||
* policy evaluator observable.
|
* policy evaluator observable.
|
||||||
* @param policy The policy being evaluated.
|
* @param policy The policy being evaluated.
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export { GeneratorHistoryService } from "./generator-history.abstraction";
|
||||||
export { GeneratorNavigationService } from "./generator-navigation.service.abstraction";
|
export { GeneratorNavigationService } from "./generator-navigation.service.abstraction";
|
||||||
export { GeneratorService } from "./generator.service.abstraction";
|
export { GeneratorService } from "./generator.service.abstraction";
|
||||||
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||||
import { GeneratedPasswordHistory } from "../password/generated-password-history";
|
import { GeneratedPasswordHistory } from "../password/generated-password-history";
|
||||||
import { PasswordGeneratorOptions } from "../password/password-generator-options";
|
import { PasswordGeneratorOptions } from "../password/password-generator-options";
|
||||||
@ -7,11 +9,12 @@ export abstract class PasswordGenerationServiceAbstraction {
|
|||||||
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||||
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||||
getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||||
|
getOptions$: () => Observable<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
enforcePasswordGeneratorPoliciesOnOptions: (
|
||||||
options: PasswordGeneratorOptions,
|
options: PasswordGeneratorOptions,
|
||||||
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||||
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
||||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||||
addHistory: (password: string) => Promise<void>;
|
addHistory: (password: string) => Promise<void>;
|
||||||
clear: (userId?: string) => Promise<void>;
|
clear: (userId?: string) => Promise<GeneratedPasswordHistory[]>;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { UsernameGeneratorOptions } from "../username/username-generation-options";
|
import { UsernameGeneratorOptions } from "../username/username-generation-options";
|
||||||
|
|
||||||
/** @deprecated Use {@link GeneratorService} with a username {@link GeneratorStrategy} instead. */
|
/** @deprecated Use {@link GeneratorService} with a username {@link GeneratorStrategy} instead. */
|
||||||
@ -8,5 +10,6 @@ export abstract class UsernameGenerationServiceAbstraction {
|
|||||||
generateCatchall: (options: UsernameGeneratorOptions) => Promise<string>;
|
generateCatchall: (options: UsernameGeneratorOptions) => Promise<string>;
|
||||||
generateForwarded: (options: UsernameGeneratorOptions) => Promise<string>;
|
generateForwarded: (options: UsernameGeneratorOptions) => Promise<string>;
|
||||||
getOptions: () => Promise<UsernameGeneratorOptions>;
|
getOptions: () => Promise<UsernameGeneratorOptions>;
|
||||||
|
getOptions$: () => Observable<UsernameGeneratorOptions>;
|
||||||
saveOptions: (options: UsernameGeneratorOptions) => Promise<void>;
|
saveOptions: (options: UsernameGeneratorOptions) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,9 @@ import { DefaultGeneratorService } from ".";
|
|||||||
function mockPolicyService(config?: { state?: BehaviorSubject<Policy[]> }) {
|
function mockPolicyService(config?: { state?: BehaviorSubject<Policy[]> }) {
|
||||||
const service = mock<PolicyService>();
|
const service = mock<PolicyService>();
|
||||||
|
|
||||||
// FIXME: swap out the mock return value when `getAll$` becomes available
|
|
||||||
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([null]);
|
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([null]);
|
||||||
service.getAll$.mockReturnValue(stateValue);
|
service.getAll$.mockReturnValue(stateValue);
|
||||||
|
|
||||||
// const stateValue = config?.state ?? new BehaviorSubject<Policy[]>(null);
|
|
||||||
// service.getAll$.mockReturnValue(stateValue);
|
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,13 @@ import { UserId } from "../../types/guid";
|
|||||||
|
|
||||||
import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions";
|
import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions";
|
||||||
|
|
||||||
|
type DefaultGeneratorServiceTuning = {
|
||||||
|
/* amount of time to keep the most recent policy after a subscription ends. Once the
|
||||||
|
* cache expires, the ignoreQty and timeoutMs settings apply to the next lookup.
|
||||||
|
*/
|
||||||
|
policyCacheMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link GeneratorServiceAbstraction} */
|
/** {@link GeneratorServiceAbstraction} */
|
||||||
export class DefaultGeneratorService<Options, Policy> implements GeneratorService<Options, Policy> {
|
export class DefaultGeneratorService<Options, Policy> implements GeneratorService<Options, Policy> {
|
||||||
/** Instantiates the generator service
|
/** Instantiates the generator service
|
||||||
@ -17,8 +24,18 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
constructor(
|
constructor(
|
||||||
private strategy: GeneratorStrategy<Options, Policy>,
|
private strategy: GeneratorStrategy<Options, Policy>,
|
||||||
private policy: PolicyService,
|
private policy: PolicyService,
|
||||||
) {}
|
tuning: Partial<DefaultGeneratorServiceTuning> = {},
|
||||||
|
) {
|
||||||
|
this.tuning = Object.assign(
|
||||||
|
{
|
||||||
|
// a minute
|
||||||
|
policyCacheMs: 60000,
|
||||||
|
},
|
||||||
|
tuning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tuning: DefaultGeneratorServiceTuning;
|
||||||
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
|
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
|
||||||
|
|
||||||
/** {@link GeneratorService.options$} */
|
/** {@link GeneratorService.options$} */
|
||||||
@ -57,7 +74,7 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
// and reduce GC pressure.
|
// and reduce GC pressure.
|
||||||
share({
|
share({
|
||||||
connector: () => new ReplaySubject(1),
|
connector: () => new ReplaySubject(1),
|
||||||
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
resetOnRefCountZero: () => timer(this.tuning.policyCacheMs),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||||
|
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { GeneratedPasswordHistory } from "../password/generated-password-history";
|
||||||
|
|
||||||
|
/** Strategy that decrypts a password history */
|
||||||
|
export class LegacyPasswordHistoryDecryptor {
|
||||||
|
constructor(
|
||||||
|
private userId: UserId,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Decrypts a password history. */
|
||||||
|
async decrypt(history: GeneratedPasswordHistory[]): Promise<GeneratedPasswordHistory[]> {
|
||||||
|
const key = await this.cryptoService.getUserKey(this.userId);
|
||||||
|
|
||||||
|
const promises = (history ?? []).map(async (item) => {
|
||||||
|
const encrypted = new EncString(item.password);
|
||||||
|
const decrypted = await this.encryptService.decryptToUtf8(encrypted, key);
|
||||||
|
return new GeneratedPasswordHistory(decrypted, item.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const decrypted = await Promise.all(promises);
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../spec";
|
import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../spec";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
@ -24,6 +24,7 @@ describe("LocalGeneratorHistoryService", () => {
|
|||||||
encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString));
|
encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString));
|
||||||
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
|
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
|
||||||
keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey));
|
keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey));
|
||||||
|
keyService.getInMemoryUserKeyFor$.mockImplementation(() => of(true as unknown as UserKey));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -5,12 +5,14 @@ import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
|||||||
import { SingleUserState, StateProvider } from "../../../platform/state";
|
import { SingleUserState, StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorHistoryService } from "../abstractions/generator-history.abstraction";
|
import { GeneratorHistoryService } from "../abstractions/generator-history.abstraction";
|
||||||
import { GENERATOR_HISTORY } from "../key-definitions";
|
import { GENERATOR_HISTORY, GENERATOR_HISTORY_BUFFER } from "../key-definitions";
|
||||||
|
import { BufferedState } from "../state/buffered-state";
|
||||||
import { PaddedDataPacker } from "../state/padded-data-packer";
|
import { PaddedDataPacker } from "../state/padded-data-packer";
|
||||||
import { SecretState } from "../state/secret-state";
|
import { SecretState } from "../state/secret-state";
|
||||||
import { UserKeyEncryptor } from "../state/user-key-encryptor";
|
import { UserKeyEncryptor } from "../state/user-key-encryptor";
|
||||||
|
|
||||||
import { GeneratedCredential } from "./generated-credential";
|
import { GeneratedCredential } from "./generated-credential";
|
||||||
|
import { LegacyPasswordHistoryDecryptor } from "./legacy-password-history-decryptor";
|
||||||
import { GeneratorCategory, HistoryServiceOptions } from "./options";
|
import { GeneratorCategory, HistoryServiceOptions } from "./options";
|
||||||
|
|
||||||
const OPTIONS_FRAME_SIZE = 2048;
|
const OPTIONS_FRAME_SIZE = 2048;
|
||||||
@ -51,7 +53,7 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
shouldUpdate: (credentials) =>
|
shouldUpdate: (credentials) =>
|
||||||
credentials?.some((f) => f.credential !== credential) ?? true,
|
!(credentials?.some((f) => f.credential === credential) ?? false),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,6 +84,13 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** {@link GeneratorHistoryService.take} */
|
||||||
|
clear = async (userId: UserId) => {
|
||||||
|
const state = this.getCredentialState(userId);
|
||||||
|
const result = (await state.update(() => null)) ?? [];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
/** {@link GeneratorHistoryService.credentials$} */
|
/** {@link GeneratorHistoryService.credentials$} */
|
||||||
credentials$ = (userId: UserId) => {
|
credentials$ = (userId: UserId) => {
|
||||||
return this.getCredentialState(userId).state$.pipe(map((credentials) => credentials ?? []));
|
return this.getCredentialState(userId).state$.pipe(map((credentials) => credentials ?? []));
|
||||||
@ -98,11 +107,12 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createSecretState(userId: UserId) {
|
private createSecretState(userId: UserId): SingleUserState<GeneratedCredential[]> {
|
||||||
// construct the encryptor
|
// construct the encryptor
|
||||||
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
||||||
const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
||||||
|
|
||||||
|
// construct the durable state
|
||||||
const state = SecretState.from<
|
const state = SecretState.from<
|
||||||
GeneratedCredential[],
|
GeneratedCredential[],
|
||||||
number,
|
number,
|
||||||
@ -111,6 +121,25 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
|
|||||||
GeneratedCredential
|
GeneratedCredential
|
||||||
>(userId, GENERATOR_HISTORY, this.stateProvider, encryptor);
|
>(userId, GENERATOR_HISTORY, this.stateProvider, encryptor);
|
||||||
|
|
||||||
return state;
|
// decryptor is just an algorithm, but it can't run until the key is available;
|
||||||
|
// providing it via an observable makes running it early impossible
|
||||||
|
const decryptor = new LegacyPasswordHistoryDecryptor(
|
||||||
|
userId,
|
||||||
|
this.keyService,
|
||||||
|
this.encryptService,
|
||||||
|
);
|
||||||
|
const decryptor$ = this.keyService
|
||||||
|
.getInMemoryUserKeyFor$(userId)
|
||||||
|
.pipe(map((key) => key && decryptor));
|
||||||
|
|
||||||
|
// move data from the old password history once decryptor is available
|
||||||
|
const buffer = new BufferedState(
|
||||||
|
this.stateProvider,
|
||||||
|
GENERATOR_HISTORY_BUFFER,
|
||||||
|
state,
|
||||||
|
decryptor$,
|
||||||
|
);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,3 +2,5 @@ export * from "./abstractions/index";
|
|||||||
export * from "./password/index";
|
export * from "./password/index";
|
||||||
|
|
||||||
export { DefaultGeneratorService } from "./default-generator.service";
|
export { DefaultGeneratorService } from "./default-generator.service";
|
||||||
|
export { legacyPasswordGenerationServiceFactory } from "./legacy-password-generation.service";
|
||||||
|
export { legacyUsernameGenerationServiceFactory } from "./legacy-username-generation.service";
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { GeneratedCredential } from "./history";
|
||||||
|
import { LegacyPasswordHistoryDecryptor } from "./history/legacy-password-history-decryptor";
|
||||||
import {
|
import {
|
||||||
EFF_USERNAME_SETTINGS,
|
EFF_USERNAME_SETTINGS,
|
||||||
CATCHALL_SETTINGS,
|
CATCHALL_SETTINGS,
|
||||||
@ -11,7 +15,15 @@ import {
|
|||||||
DUCK_DUCK_GO_FORWARDER,
|
DUCK_DUCK_GO_FORWARDER,
|
||||||
ADDY_IO_FORWARDER,
|
ADDY_IO_FORWARDER,
|
||||||
GENERATOR_SETTINGS,
|
GENERATOR_SETTINGS,
|
||||||
|
ADDY_IO_BUFFER,
|
||||||
|
DUCK_DUCK_GO_BUFFER,
|
||||||
|
FASTMAIL_BUFFER,
|
||||||
|
FIREFOX_RELAY_BUFFER,
|
||||||
|
FORWARD_EMAIL_BUFFER,
|
||||||
|
SIMPLE_LOGIN_BUFFER,
|
||||||
|
GENERATOR_HISTORY_BUFFER,
|
||||||
} from "./key-definitions";
|
} from "./key-definitions";
|
||||||
|
import { GeneratedPasswordHistory } from "./password";
|
||||||
|
|
||||||
describe("Key definitions", () => {
|
describe("Key definitions", () => {
|
||||||
describe("GENERATOR_SETTINGS", () => {
|
describe("GENERATOR_SETTINGS", () => {
|
||||||
@ -109,4 +121,121 @@ describe("Key definitions", () => {
|
|||||||
expect(result).toBe(value);
|
expect(result).toBe(value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ADDY_IO_BUFFER", () => {
|
||||||
|
it("should pass through deserialization", () => {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
const result = ADDY_IO_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DUCK_DUCK_GO_BUFFER", () => {
|
||||||
|
it("should pass through deserialization", () => {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
const result = DUCK_DUCK_GO_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FASTMAIL_BUFFER", () => {
|
||||||
|
it("should pass through deserialization", () => {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
const result = FASTMAIL_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FIREFOX_RELAY_BUFFER", () => {
|
||||||
|
it("should pass through deserialization", () => {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
const result = FIREFOX_RELAY_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FORWARD_EMAIL_BUFFER", () => {
|
||||||
|
it("should pass through deserialization", () => {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
const result = FORWARD_EMAIL_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SIMPLE_LOGIN_BUFFER", () => {
|
||||||
|
it("should pass through deserialization", () => {
|
||||||
|
const value: any = {};
|
||||||
|
|
||||||
|
const result = SIMPLE_LOGIN_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GENERATOR_HISTORY_BUFFER", () => {
|
||||||
|
describe("options.deserializer", () => {
|
||||||
|
it("should deserialize generated password history", () => {
|
||||||
|
const value: any = [{ password: "foo", date: 1 }];
|
||||||
|
|
||||||
|
const [result] = GENERATOR_HISTORY_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toEqual(value[0]);
|
||||||
|
expect(result).toBeInstanceOf(GeneratedPasswordHistory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([[undefined], [null]])("should ignore nullish (= %p) history", (value: any) => {
|
||||||
|
const result = GENERATOR_HISTORY_BUFFER.options.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should map generated password history to generated credentials", async () => {
|
||||||
|
const value: any = [new GeneratedPasswordHistory("foo", 1)];
|
||||||
|
const decryptor = mock<LegacyPasswordHistoryDecryptor>({
|
||||||
|
decrypt(value) {
|
||||||
|
return Promise.resolve(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [result] = await GENERATOR_HISTORY_BUFFER.map(value, decryptor);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
credential: "foo",
|
||||||
|
category: "password",
|
||||||
|
generationDate: new Date(1),
|
||||||
|
});
|
||||||
|
expect(result).toBeInstanceOf(GeneratedCredential);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValid", () => {
|
||||||
|
it("should accept histories with at least one entry", async () => {
|
||||||
|
const value: any = [new GeneratedPasswordHistory("foo", 1)];
|
||||||
|
const decryptor = {} as any;
|
||||||
|
|
||||||
|
const result = await GENERATOR_HISTORY_BUFFER.isValid(value, decryptor);
|
||||||
|
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject histories with no entries", async () => {
|
||||||
|
const value: any = [];
|
||||||
|
const decryptor = {} as any;
|
||||||
|
|
||||||
|
const result = await GENERATOR_HISTORY_BUFFER.isValid(value, decryptor);
|
||||||
|
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { GENERATOR_DISK, GENERATOR_MEMORY, UserKeyDefinition } from "../../platform/state";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { GENERATOR_DISK, UserKeyDefinition } from "../../platform/state";
|
||||||
|
|
||||||
import { GeneratedCredential } from "./history/generated-credential";
|
import { GeneratedCredential } from "./history/generated-credential";
|
||||||
|
import { LegacyPasswordHistoryDecryptor } from "./history/legacy-password-history-decryptor";
|
||||||
import { GeneratorNavigation } from "./navigation/generator-navigation";
|
import { GeneratorNavigation } from "./navigation/generator-navigation";
|
||||||
import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options";
|
import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options";
|
||||||
|
import { GeneratedPasswordHistory } from "./password/generated-password-history";
|
||||||
import { PasswordGenerationOptions } from "./password/password-generation-options";
|
import { PasswordGenerationOptions } from "./password/password-generation-options";
|
||||||
|
import { BufferedKeyDefinition } from "./state/buffered-key-definition";
|
||||||
import { SecretClassifier } from "./state/secret-classifier";
|
import { SecretClassifier } from "./state/secret-classifier";
|
||||||
import { SecretKeyDefinition } from "./state/secret-key-definition";
|
import { SecretKeyDefinition } from "./state/secret-key-definition";
|
||||||
import { CatchallGenerationOptions } from "./username/catchall-generator-options";
|
import { CatchallGenerationOptions } from "./username/catchall-generator-options";
|
||||||
@ -18,11 +23,11 @@ import { SubaddressGenerationOptions } from "./username/subaddress-generator-opt
|
|||||||
|
|
||||||
/** plaintext password generation options */
|
/** plaintext password generation options */
|
||||||
export const GENERATOR_SETTINGS = new UserKeyDefinition<GeneratorNavigation>(
|
export const GENERATOR_SETTINGS = new UserKeyDefinition<GeneratorNavigation>(
|
||||||
GENERATOR_MEMORY,
|
GENERATOR_DISK,
|
||||||
"generatorSettings",
|
"generatorSettings",
|
||||||
{
|
{
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["lock", "logout"],
|
clearOn: ["logout"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -136,6 +141,66 @@ export const SIMPLE_LOGIN_FORWARDER = new UserKeyDefinition<SelfHostedApiOptions
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.AddyIo} */
|
||||||
|
export const ADDY_IO_BUFFER = new BufferedKeyDefinition<SelfHostedApiOptions & EmailDomainOptions>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"addyIoBuffer",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.DuckDuckGo} */
|
||||||
|
export const DUCK_DUCK_GO_BUFFER = new BufferedKeyDefinition<ApiOptions>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"duckDuckGoBuffer",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.FastMail} */
|
||||||
|
export const FASTMAIL_BUFFER = new BufferedKeyDefinition<ApiOptions & EmailPrefixOptions>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"fastmailBuffer",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.FireFoxRelay} */
|
||||||
|
export const FIREFOX_RELAY_BUFFER = new BufferedKeyDefinition<ApiOptions>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"firefoxRelayBuffer",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link Forwarders.ForwardEmail} */
|
||||||
|
export const FORWARD_EMAIL_BUFFER = new BufferedKeyDefinition<ApiOptions & EmailDomainOptions>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"forwardEmailBuffer",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** backing store configuration for {@link forwarders.SimpleLogin} */
|
||||||
|
export const SIMPLE_LOGIN_BUFFER = new BufferedKeyDefinition<SelfHostedApiOptions>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"simpleLoginBuffer",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/** encrypted password generation history */
|
/** encrypted password generation history */
|
||||||
export const GENERATOR_HISTORY = SecretKeyDefinition.array(
|
export const GENERATOR_HISTORY = SecretKeyDefinition.array(
|
||||||
GENERATOR_DISK,
|
GENERATOR_DISK,
|
||||||
@ -146,3 +211,24 @@ export const GENERATOR_HISTORY = SecretKeyDefinition.array(
|
|||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** encrypted password generation history subject to migration */
|
||||||
|
export const GENERATOR_HISTORY_BUFFER = new BufferedKeyDefinition<
|
||||||
|
GeneratedPasswordHistory[],
|
||||||
|
GeneratedCredential[],
|
||||||
|
LegacyPasswordHistoryDecryptor
|
||||||
|
>(GENERATOR_DISK, "localGeneratorHistoryBuffer", {
|
||||||
|
deserializer(history) {
|
||||||
|
const items = history as Jsonify<GeneratedPasswordHistory>[];
|
||||||
|
return items?.map((h) => new GeneratedPasswordHistory(h.password, h.date));
|
||||||
|
},
|
||||||
|
async isValid(history) {
|
||||||
|
return history.length ? true : false;
|
||||||
|
},
|
||||||
|
async map(history, decryptor) {
|
||||||
|
const credentials = await decryptor.decrypt(history);
|
||||||
|
const mapped = credentials.map((c) => new GeneratedCredential(c.password, "password", c.date));
|
||||||
|
return mapped;
|
||||||
|
},
|
||||||
|
clearOn: ["logout"],
|
||||||
|
});
|
||||||
|
@ -8,7 +8,12 @@ import { of } from "rxjs";
|
|||||||
import { mockAccountServiceWith } from "../../../spec";
|
import { mockAccountServiceWith } from "../../../spec";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { GeneratorNavigationService, GeneratorService } from "./abstractions";
|
import {
|
||||||
|
GeneratorHistoryService,
|
||||||
|
GeneratorNavigationService,
|
||||||
|
GeneratorService,
|
||||||
|
} from "./abstractions";
|
||||||
|
import { GeneratedCredential } from "./history";
|
||||||
import { LegacyPasswordGenerationService } from "./legacy-password-generation.service";
|
import { LegacyPasswordGenerationService } from "./legacy-password-generation.service";
|
||||||
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation";
|
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation";
|
||||||
import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator";
|
import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator";
|
||||||
@ -22,6 +27,7 @@ import {
|
|||||||
import { DisabledPassphraseGeneratorPolicy } from "./passphrase/passphrase-generator-policy";
|
import { DisabledPassphraseGeneratorPolicy } from "./passphrase/passphrase-generator-policy";
|
||||||
import {
|
import {
|
||||||
DefaultPasswordGenerationOptions,
|
DefaultPasswordGenerationOptions,
|
||||||
|
GeneratedPasswordHistory,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
PasswordGeneratorOptions,
|
PasswordGeneratorOptions,
|
||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
@ -97,10 +103,10 @@ function createNavigationGenerator(
|
|||||||
defaults$(id: UserId) {
|
defaults$(id: UserId) {
|
||||||
return of(DefaultGeneratorNavigation);
|
return of(DefaultGeneratorNavigation);
|
||||||
},
|
},
|
||||||
saveOptions(userId, options) {
|
saveOptions: jest.fn((userId, options) => {
|
||||||
savedOptions = options;
|
savedOptions = options;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return generator;
|
return generator;
|
||||||
@ -113,7 +119,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
describe("generatePassword", () => {
|
describe("generatePassword", () => {
|
||||||
it("invokes the inner password generator to generate passwords", async () => {
|
it("invokes the inner password generator to generate passwords", async () => {
|
||||||
const innerPassword = createPasswordGenerator();
|
const innerPassword = createPasswordGenerator();
|
||||||
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null);
|
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null);
|
||||||
const options = { type: "password" } as PasswordGeneratorOptions;
|
const options = { type: "password" } as PasswordGeneratorOptions;
|
||||||
|
|
||||||
await generator.generatePassword(options);
|
await generator.generatePassword(options);
|
||||||
@ -123,7 +129,13 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
|
|
||||||
it("invokes the inner passphrase generator to generate passphrases", async () => {
|
it("invokes the inner passphrase generator to generate passphrases", async () => {
|
||||||
const innerPassphrase = createPassphraseGenerator();
|
const innerPassphrase = createPassphraseGenerator();
|
||||||
const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase);
|
const generator = new LegacyPasswordGenerationService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
innerPassphrase,
|
||||||
|
null,
|
||||||
|
);
|
||||||
const options = { type: "passphrase" } as PasswordGeneratorOptions;
|
const options = { type: "passphrase" } as PasswordGeneratorOptions;
|
||||||
|
|
||||||
await generator.generatePassword(options);
|
await generator.generatePassword(options);
|
||||||
@ -135,7 +147,13 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
describe("generatePassphrase", () => {
|
describe("generatePassphrase", () => {
|
||||||
it("invokes the inner passphrase generator", async () => {
|
it("invokes the inner passphrase generator", async () => {
|
||||||
const innerPassphrase = createPassphraseGenerator();
|
const innerPassphrase = createPassphraseGenerator();
|
||||||
const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase);
|
const generator = new LegacyPasswordGenerationService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
innerPassphrase,
|
||||||
|
null,
|
||||||
|
);
|
||||||
const options = {} as PasswordGeneratorOptions;
|
const options = {} as PasswordGeneratorOptions;
|
||||||
|
|
||||||
await generator.generatePassphrase(options);
|
await generator.generatePassphrase(options);
|
||||||
@ -157,7 +175,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
number: true,
|
number: true,
|
||||||
minNumber: 3,
|
minNumber: 3,
|
||||||
special: false,
|
special: false,
|
||||||
minSpecial: 4,
|
minSpecial: 0,
|
||||||
});
|
});
|
||||||
const innerPassphrase = createPassphraseGenerator({
|
const innerPassphrase = createPassphraseGenerator({
|
||||||
numWords: 10,
|
numWords: 10,
|
||||||
@ -176,29 +194,29 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [result] = await generator.getOptions();
|
const [result] = await generator.getOptions();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: "passphrase",
|
type: "passphrase",
|
||||||
username: "word",
|
|
||||||
forwarder: "simplelogin",
|
|
||||||
length: 29,
|
length: 29,
|
||||||
minLength: 20,
|
minLength: 5,
|
||||||
ambiguous: false,
|
ambiguous: false,
|
||||||
uppercase: true,
|
uppercase: true,
|
||||||
minUppercase: 1,
|
minUppercase: 1,
|
||||||
lowercase: false,
|
lowercase: false,
|
||||||
minLowercase: 2,
|
minLowercase: 0,
|
||||||
number: true,
|
number: true,
|
||||||
minNumber: 3,
|
minNumber: 3,
|
||||||
special: false,
|
special: false,
|
||||||
minSpecial: 4,
|
minSpecial: 0,
|
||||||
numWords: 10,
|
numWords: 10,
|
||||||
wordSeparator: "-",
|
wordSeparator: "-",
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
includeNumber: false,
|
includeNumber: false,
|
||||||
|
policyUpdated: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -212,14 +230,18 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [result] = await generator.getOptions();
|
const [result] = await generator.getOptions();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
...DefaultGeneratorNavigation,
|
type: DefaultGeneratorNavigation.type,
|
||||||
...DefaultPassphraseGenerationOptions,
|
...DefaultPassphraseGenerationOptions,
|
||||||
...DefaultPasswordGenerationOptions,
|
...DefaultPasswordGenerationOptions,
|
||||||
|
minLowercase: 1,
|
||||||
|
minUppercase: 1,
|
||||||
|
policyUpdated: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -256,6 +278,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [, policy] = await generator.getOptions();
|
const [, policy] = await generator.getOptions();
|
||||||
@ -301,6 +324,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
||||||
@ -340,6 +364,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
|
||||||
@ -385,6 +410,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
|
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
|
||||||
@ -416,22 +442,21 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
const options = {
|
const options = {
|
||||||
type: "password" as const,
|
type: "password" as const,
|
||||||
username: "word" as const,
|
|
||||||
forwarder: "simplelogin" as const,
|
|
||||||
length: 29,
|
length: 29,
|
||||||
minLength: 20,
|
minLength: 5,
|
||||||
ambiguous: false,
|
ambiguous: false,
|
||||||
uppercase: true,
|
uppercase: true,
|
||||||
minUppercase: 1,
|
minUppercase: 1,
|
||||||
lowercase: false,
|
lowercase: false,
|
||||||
minLowercase: 2,
|
minLowercase: 0,
|
||||||
number: true,
|
number: true,
|
||||||
minNumber: 3,
|
minNumber: 3,
|
||||||
special: false,
|
special: false,
|
||||||
minSpecial: 4,
|
minSpecial: 0,
|
||||||
};
|
};
|
||||||
await generator.saveOptions(options);
|
await generator.saveOptions(options);
|
||||||
|
|
||||||
@ -450,11 +475,10 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
navigation,
|
navigation,
|
||||||
innerPassword,
|
innerPassword,
|
||||||
innerPassphrase,
|
innerPassphrase,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
const options = {
|
const options = {
|
||||||
type: "passphrase" as const,
|
type: "passphrase" as const,
|
||||||
username: "word" as const,
|
|
||||||
forwarder: "simplelogin" as const,
|
|
||||||
numWords: 10,
|
numWords: 10,
|
||||||
wordSeparator: "-",
|
wordSeparator: "-",
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
@ -466,5 +490,78 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
|
|
||||||
expect(result).toMatchObject(options);
|
expect(result).toMatchObject(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves saved navigation options", async () => {
|
||||||
|
const innerPassword = createPasswordGenerator();
|
||||||
|
const innerPassphrase = createPassphraseGenerator();
|
||||||
|
const navigation = createNavigationGenerator({
|
||||||
|
type: "password",
|
||||||
|
username: "forwarded",
|
||||||
|
forwarder: "firefoxrelay",
|
||||||
|
});
|
||||||
|
const accountService = mockAccountServiceWith(SomeUser);
|
||||||
|
const generator = new LegacyPasswordGenerationService(
|
||||||
|
accountService,
|
||||||
|
navigation,
|
||||||
|
innerPassword,
|
||||||
|
innerPassphrase,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const options = {
|
||||||
|
type: "passphrase" as const,
|
||||||
|
numWords: 10,
|
||||||
|
wordSeparator: "-",
|
||||||
|
capitalize: true,
|
||||||
|
includeNumber: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
await generator.saveOptions(options);
|
||||||
|
|
||||||
|
expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, {
|
||||||
|
type: "passphrase",
|
||||||
|
username: "forwarded",
|
||||||
|
forwarder: "firefoxrelay",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getHistory", () => {
|
||||||
|
it("gets the active user's history from the history service", async () => {
|
||||||
|
const history = mock<GeneratorHistoryService>();
|
||||||
|
history.credentials$.mockReturnValue(
|
||||||
|
of([new GeneratedCredential("foo", "password", new Date(100))]),
|
||||||
|
);
|
||||||
|
const accountService = mockAccountServiceWith(SomeUser);
|
||||||
|
const generator = new LegacyPasswordGenerationService(
|
||||||
|
accountService,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
history,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await generator.getHistory();
|
||||||
|
|
||||||
|
expect(history.credentials$).toHaveBeenCalledWith(SomeUser);
|
||||||
|
expect(result).toEqual([new GeneratedPasswordHistory("foo", 100)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addHistory", () => {
|
||||||
|
it("adds a history item as a password credential", async () => {
|
||||||
|
const history = mock<GeneratorHistoryService>();
|
||||||
|
const accountService = mockAccountServiceWith(SomeUser);
|
||||||
|
const generator = new LegacyPasswordGenerationService(
|
||||||
|
accountService,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
history,
|
||||||
|
);
|
||||||
|
|
||||||
|
await generator.addHistory("foo");
|
||||||
|
|
||||||
|
expect(history.track).toHaveBeenCalledWith(SomeUser, "foo", "password");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,21 +1,42 @@
|
|||||||
import { concatMap, zip, map, firstValueFrom } from "rxjs";
|
import {
|
||||||
|
concatMap,
|
||||||
|
zip,
|
||||||
|
map,
|
||||||
|
firstValueFrom,
|
||||||
|
combineLatest,
|
||||||
|
pairwise,
|
||||||
|
of,
|
||||||
|
concat,
|
||||||
|
Observable,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PasswordGeneratorPolicyOptions } from "../../admin-console/models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "../../admin-console/models/domain/password-generator-policy-options";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||||
import { StateProvider } from "../../platform/state";
|
import { StateProvider } from "../../platform/state";
|
||||||
|
|
||||||
import { GeneratorService, GeneratorNavigationService } from "./abstractions";
|
import {
|
||||||
|
GeneratorHistoryService,
|
||||||
|
GeneratorService,
|
||||||
|
GeneratorNavigationService,
|
||||||
|
PolicyEvaluator,
|
||||||
|
} from "./abstractions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "./abstractions/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "./abstractions/password-generation.service.abstraction";
|
||||||
import { DefaultGeneratorService } from "./default-generator.service";
|
import { DefaultGeneratorService } from "./default-generator.service";
|
||||||
|
import { GeneratedCredential } from "./history";
|
||||||
|
import { LocalGeneratorHistoryService } from "./history/local-generator-history.service";
|
||||||
|
import { GeneratorNavigation } from "./navigation";
|
||||||
import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service";
|
import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service";
|
||||||
|
import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy";
|
||||||
import {
|
import {
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy,
|
PassphraseGeneratorPolicy,
|
||||||
PassphraseGeneratorStrategy,
|
PassphraseGeneratorStrategy,
|
||||||
} from "./passphrase";
|
} from "./passphrase";
|
||||||
import {
|
import {
|
||||||
|
GeneratedPasswordHistory,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
PasswordGenerationService,
|
PasswordGenerationService,
|
||||||
PasswordGeneratorOptions,
|
PasswordGeneratorOptions,
|
||||||
@ -23,7 +44,15 @@ import {
|
|||||||
PasswordGeneratorStrategy,
|
PasswordGeneratorStrategy,
|
||||||
} from "./password";
|
} from "./password";
|
||||||
|
|
||||||
|
type MappedOptions = {
|
||||||
|
generator: GeneratorNavigation;
|
||||||
|
password: PasswordGenerationOptions;
|
||||||
|
passphrase: PassphraseGenerationOptions;
|
||||||
|
policyUpdated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function legacyPasswordGenerationServiceFactory(
|
export function legacyPasswordGenerationServiceFactory(
|
||||||
|
encryptService: EncryptService,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
@ -45,7 +74,15 @@ export function legacyPasswordGenerationServiceFactory(
|
|||||||
|
|
||||||
const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService);
|
const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService);
|
||||||
|
|
||||||
return new LegacyPasswordGenerationService(accountService, navigation, passwords, passphrases);
|
const history = new LocalGeneratorHistoryService(encryptService, cryptoService, stateProvider);
|
||||||
|
|
||||||
|
return new LegacyPasswordGenerationService(
|
||||||
|
accountService,
|
||||||
|
navigation,
|
||||||
|
passwords,
|
||||||
|
passphrases,
|
||||||
|
history,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Adapts the generator 2.0 design to 1.0 angular services. */
|
/** Adapts the generator 2.0 design to 1.0 angular services. */
|
||||||
@ -61,6 +98,7 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic
|
|||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy
|
PassphraseGeneratorPolicy
|
||||||
>,
|
>,
|
||||||
|
private readonly history: GeneratorHistoryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
generatePassword(options: PasswordGeneratorOptions) {
|
generatePassword(options: PasswordGeneratorOptions) {
|
||||||
@ -75,21 +113,112 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic
|
|||||||
return this.passphrases.generate(options);
|
return this.passphrases.generate(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOptions() {
|
private getRawOptions$() {
|
||||||
const options$ = this.accountService.activeAccount$.pipe(
|
// give the typechecker a nudge to avoid "implicit any" errors
|
||||||
|
type RawOptionsIntermediateType = [
|
||||||
|
PasswordGenerationOptions,
|
||||||
|
PasswordGenerationOptions,
|
||||||
|
[PolicyEvaluator<PasswordGeneratorPolicy, PasswordGenerationOptions>, number],
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
[PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>, number],
|
||||||
|
GeneratorNavigation,
|
||||||
|
GeneratorNavigation,
|
||||||
|
[PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>, number],
|
||||||
|
];
|
||||||
|
|
||||||
|
function withSequenceNumber<T>(observable$: Observable<T>) {
|
||||||
|
return observable$.pipe(map((evaluator, i) => [evaluator, i] as const));
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial array ensures that destructuring never fails; sequence numbers
|
||||||
|
// set to `-1` so that the first update reflects that the policy changed from
|
||||||
|
// "unknown" to "whatever was provided by the service". This needs to be called
|
||||||
|
// each time the active user changes or the `concat` will block.
|
||||||
|
function initial$() {
|
||||||
|
const initial: RawOptionsIntermediateType = [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[null, -1],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[null, -1],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[null, -1],
|
||||||
|
];
|
||||||
|
|
||||||
|
return of(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
function intermediatePairsToRawOptions([previous, current]: [
|
||||||
|
RawOptionsIntermediateType,
|
||||||
|
RawOptionsIntermediateType,
|
||||||
|
]) {
|
||||||
|
const [, , [, passwordPrevious], , , [, passphrasePrevious], , , [, generatorPrevious]] =
|
||||||
|
previous;
|
||||||
|
const [
|
||||||
|
passwordOptions,
|
||||||
|
passwordDefaults,
|
||||||
|
[passwordEvaluator, passwordCurrent],
|
||||||
|
passphraseOptions,
|
||||||
|
passphraseDefaults,
|
||||||
|
[passphraseEvaluator, passphraseCurrent],
|
||||||
|
generatorOptions,
|
||||||
|
generatorDefaults,
|
||||||
|
[generatorEvaluator, generatorCurrent],
|
||||||
|
] = current;
|
||||||
|
|
||||||
|
// when any of the sequence numbers change, the emission occurs as the result of
|
||||||
|
// a policy update
|
||||||
|
const policyEmitted =
|
||||||
|
passwordPrevious < passwordCurrent ||
|
||||||
|
passphrasePrevious < passphraseCurrent ||
|
||||||
|
generatorPrevious < generatorCurrent;
|
||||||
|
|
||||||
|
const result = [
|
||||||
|
passwordOptions,
|
||||||
|
passwordDefaults,
|
||||||
|
passwordEvaluator,
|
||||||
|
passphraseOptions,
|
||||||
|
passphraseDefaults,
|
||||||
|
passphraseEvaluator,
|
||||||
|
generatorOptions,
|
||||||
|
generatorDefaults,
|
||||||
|
generatorEvaluator,
|
||||||
|
policyEmitted,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// look upon my works, ye mighty, and despair!
|
||||||
|
const rawOptions$ = this.accountService.activeAccount$.pipe(
|
||||||
concatMap((activeUser) =>
|
concatMap((activeUser) =>
|
||||||
zip(
|
concat(
|
||||||
|
initial$(),
|
||||||
|
combineLatest([
|
||||||
this.passwords.options$(activeUser.id),
|
this.passwords.options$(activeUser.id),
|
||||||
this.passwords.defaults$(activeUser.id),
|
this.passwords.defaults$(activeUser.id),
|
||||||
this.passwords.evaluator$(activeUser.id),
|
withSequenceNumber(this.passwords.evaluator$(activeUser.id)),
|
||||||
this.passphrases.options$(activeUser.id),
|
this.passphrases.options$(activeUser.id),
|
||||||
this.passphrases.defaults$(activeUser.id),
|
this.passphrases.defaults$(activeUser.id),
|
||||||
this.passphrases.evaluator$(activeUser.id),
|
withSequenceNumber(this.passphrases.evaluator$(activeUser.id)),
|
||||||
this.navigation.options$(activeUser.id),
|
this.navigation.options$(activeUser.id),
|
||||||
this.navigation.defaults$(activeUser.id),
|
this.navigation.defaults$(activeUser.id),
|
||||||
this.navigation.evaluator$(activeUser.id),
|
withSequenceNumber(this.navigation.evaluator$(activeUser.id)),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
pairwise(),
|
||||||
|
map(intermediatePairsToRawOptions),
|
||||||
|
);
|
||||||
|
|
||||||
|
return rawOptions$;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions$() {
|
||||||
|
const options$ = this.getRawOptions$().pipe(
|
||||||
map(
|
map(
|
||||||
([
|
([
|
||||||
passwordOptions,
|
passwordOptions,
|
||||||
@ -101,14 +230,25 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic
|
|||||||
generatorOptions,
|
generatorOptions,
|
||||||
generatorDefaults,
|
generatorDefaults,
|
||||||
generatorEvaluator,
|
generatorEvaluator,
|
||||||
|
policyUpdated,
|
||||||
]) => {
|
]) => {
|
||||||
const options: PasswordGeneratorOptions = Object.assign(
|
const passwordOptionsWithPolicy = passwordEvaluator.applyPolicy(
|
||||||
{},
|
|
||||||
passwordOptions ?? passwordDefaults,
|
passwordOptions ?? passwordDefaults,
|
||||||
|
);
|
||||||
|
const passphraseOptionsWithPolicy = passphraseEvaluator.applyPolicy(
|
||||||
passphraseOptions ?? passphraseDefaults,
|
passphraseOptions ?? passphraseDefaults,
|
||||||
|
);
|
||||||
|
const generatorOptionsWithPolicy = generatorEvaluator.applyPolicy(
|
||||||
generatorOptions ?? generatorDefaults,
|
generatorOptions ?? generatorDefaults,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const options = this.toPasswordGeneratorOptions({
|
||||||
|
password: passwordEvaluator.sanitize(passwordOptionsWithPolicy),
|
||||||
|
passphrase: passphraseEvaluator.sanitize(passphraseOptionsWithPolicy),
|
||||||
|
generator: generatorEvaluator.sanitize(generatorOptionsWithPolicy),
|
||||||
|
policyUpdated,
|
||||||
|
});
|
||||||
|
|
||||||
const policy = Object.assign(
|
const policy = Object.assign(
|
||||||
new PasswordGeneratorPolicyOptions(),
|
new PasswordGeneratorPolicyOptions(),
|
||||||
passwordEvaluator.policy,
|
passwordEvaluator.policy,
|
||||||
@ -116,13 +256,16 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic
|
|||||||
generatorEvaluator.policy,
|
generatorEvaluator.policy,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [options, policy] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions];
|
return [options, policy] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions];
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = await firstValueFrom(options$);
|
return options$;
|
||||||
return options;
|
}
|
||||||
|
|
||||||
|
async getOptions() {
|
||||||
|
return await firstValueFrom(this.getOptions$());
|
||||||
}
|
}
|
||||||
|
|
||||||
async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) {
|
async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) {
|
||||||
@ -164,21 +307,103 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic
|
|||||||
// callers assume this function updates the options parameter
|
// callers assume this function updates the options parameter
|
||||||
Object.assign(options, sanitized),
|
Object.assign(options, sanitized),
|
||||||
policy,
|
policy,
|
||||||
] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions];
|
] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions];
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveOptions(options: PasswordGeneratorOptions) {
|
async saveOptions(options: PasswordGeneratorOptions) {
|
||||||
|
const stored = this.toStoredOptions(options);
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
await this.navigation.saveOptions(activeAccount.id, options);
|
// generator settings needs to preserve whether password or passphrase is selected,
|
||||||
if (options.type === "password") {
|
// so `navigationOptions` is mutated.
|
||||||
await this.passwords.saveOptions(activeAccount.id, options);
|
const navigationOptions$ = zip(
|
||||||
} else {
|
this.navigation.options$(activeAccount.id),
|
||||||
await this.passphrases.saveOptions(activeAccount.id, options);
|
this.navigation.defaults$(activeAccount.id),
|
||||||
|
).pipe(map(([options, defaults]) => options ?? defaults));
|
||||||
|
let navigationOptions = await firstValueFrom(navigationOptions$);
|
||||||
|
navigationOptions = Object.assign(navigationOptions, stored.generator);
|
||||||
|
await this.navigation.saveOptions(activeAccount.id, navigationOptions);
|
||||||
|
|
||||||
|
// overwrite all other settings with latest values
|
||||||
|
await this.passwords.saveOptions(activeAccount.id, stored.password);
|
||||||
|
await this.passphrases.saveOptions(activeAccount.id, stored.passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toStoredOptions(options: PasswordGeneratorOptions): MappedOptions {
|
||||||
|
return {
|
||||||
|
generator: {
|
||||||
|
type: options.type,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
length: options.length,
|
||||||
|
minLength: options.minLength,
|
||||||
|
ambiguous: options.ambiguous,
|
||||||
|
uppercase: options.uppercase,
|
||||||
|
minUppercase: options.minUppercase,
|
||||||
|
lowercase: options.lowercase,
|
||||||
|
minLowercase: options.minLowercase,
|
||||||
|
number: options.number,
|
||||||
|
minNumber: options.minNumber,
|
||||||
|
special: options.special,
|
||||||
|
minSpecial: options.minSpecial,
|
||||||
|
},
|
||||||
|
passphrase: {
|
||||||
|
numWords: options.numWords,
|
||||||
|
wordSeparator: options.wordSeparator,
|
||||||
|
capitalize: options.capitalize,
|
||||||
|
includeNumber: options.includeNumber,
|
||||||
|
},
|
||||||
|
policyUpdated: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toPasswordGeneratorOptions(options: MappedOptions): PasswordGeneratorOptions {
|
||||||
|
return {
|
||||||
|
type: options.generator.type,
|
||||||
|
length: options.password.length,
|
||||||
|
minLength: options.password.minLength,
|
||||||
|
ambiguous: options.password.ambiguous,
|
||||||
|
uppercase: options.password.uppercase,
|
||||||
|
minUppercase: options.password.minUppercase,
|
||||||
|
lowercase: options.password.lowercase,
|
||||||
|
minLowercase: options.password.minLowercase,
|
||||||
|
number: options.password.number,
|
||||||
|
minNumber: options.password.minNumber,
|
||||||
|
special: options.password.special,
|
||||||
|
minSpecial: options.password.minSpecial,
|
||||||
|
numWords: options.passphrase.numWords,
|
||||||
|
wordSeparator: options.passphrase.wordSeparator,
|
||||||
|
capitalize: options.passphrase.capitalize,
|
||||||
|
includeNumber: options.passphrase.includeNumber,
|
||||||
|
policyUpdated: options.policyUpdated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistory() {
|
||||||
|
const history = this.accountService.activeAccount$.pipe(
|
||||||
|
concatMap((account) => this.history.credentials$(account.id)),
|
||||||
|
map((history) => history.map(toGeneratedPasswordHistory)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return firstValueFrom(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addHistory(password: string) {
|
||||||
|
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
// legacy service doesn't distinguish credential types
|
||||||
|
await this.history.track(account.id, password, "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const history$ = this.accountService.activeAccount$.pipe(
|
||||||
|
concatMap((account) => this.history.clear(account.id)),
|
||||||
|
map((history) => history.map(toGeneratedPasswordHistory)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return firstValueFrom(history$);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getHistory: () => Promise<any[]>;
|
function toGeneratedPasswordHistory(value: GeneratedCredential) {
|
||||||
addHistory: (password: string) => Promise<void>;
|
return new GeneratedPasswordHistory(value.credential, value.generationDate.valueOf());
|
||||||
clear: (userId?: string) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
it("should generate a catchall username", async () => {
|
it("should generate a catchall username", async () => {
|
||||||
const options = { type: "catchall" } as UsernameGeneratorOptions;
|
const options = { type: "catchall" } as UsernameGeneratorOptions;
|
||||||
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
|
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
|
||||||
catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com"));
|
catchall.generate.mockResolvedValue("catchall@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -118,7 +118,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
it("should generate an EFF word username", async () => {
|
it("should generate an EFF word username", async () => {
|
||||||
const options = { type: "word" } as UsernameGeneratorOptions;
|
const options = { type: "word" } as UsernameGeneratorOptions;
|
||||||
const effWord = createGenerator<EffUsernameGenerationOptions>(null, null);
|
const effWord = createGenerator<EffUsernameGenerationOptions>(null, null);
|
||||||
effWord.generate.mockReturnValue(Promise.resolve("eff word"));
|
effWord.generate.mockResolvedValue("eff word");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -142,7 +142,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
it("should generate a subaddress username", async () => {
|
it("should generate a subaddress username", async () => {
|
||||||
const options = { type: "subaddress" } as UsernameGeneratorOptions;
|
const options = { type: "subaddress" } as UsernameGeneratorOptions;
|
||||||
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
|
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
|
||||||
subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com"));
|
subaddress.generate.mockResolvedValue("subaddress@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -170,7 +170,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
forwardedService: Forwarders.AddyIo.id,
|
forwardedService: Forwarders.AddyIo.id,
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
|
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
|
||||||
addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com"));
|
addyIo.generate.mockResolvedValue("addyio@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -196,7 +196,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
it("should generate a catchall username", async () => {
|
it("should generate a catchall username", async () => {
|
||||||
const options = { type: "catchall" } as UsernameGeneratorOptions;
|
const options = { type: "catchall" } as UsernameGeneratorOptions;
|
||||||
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
|
const catchall = createGenerator<CatchallGenerationOptions>(null, null);
|
||||||
catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com"));
|
catchall.generate.mockResolvedValue("catchall@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -222,7 +222,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
it("should generate a subaddress username", async () => {
|
it("should generate a subaddress username", async () => {
|
||||||
const options = { type: "subaddress" } as UsernameGeneratorOptions;
|
const options = { type: "subaddress" } as UsernameGeneratorOptions;
|
||||||
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
|
const subaddress = createGenerator<SubaddressGenerationOptions>(null, null);
|
||||||
subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com"));
|
subaddress.generate.mockResolvedValue("subaddress@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -254,7 +254,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
website: "example.com",
|
website: "example.com",
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
|
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
|
||||||
addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com"));
|
addyIo.generate.mockResolvedValue("addyio@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -287,7 +287,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
website: "example.com",
|
website: "example.com",
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const duckDuckGo = createGenerator<ApiOptions>(null, null);
|
const duckDuckGo = createGenerator<ApiOptions>(null, null);
|
||||||
duckDuckGo.generate.mockReturnValue(Promise.resolve("ddg@example.com"));
|
duckDuckGo.generate.mockResolvedValue("ddg@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -318,7 +318,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
website: "example.com",
|
website: "example.com",
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, null);
|
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, null);
|
||||||
fastmail.generate.mockReturnValue(Promise.resolve("fastmail@example.com"));
|
fastmail.generate.mockResolvedValue("fastmail@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -349,7 +349,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
website: "example.com",
|
website: "example.com",
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const firefoxRelay = createGenerator<ApiOptions>(null, null);
|
const firefoxRelay = createGenerator<ApiOptions>(null, null);
|
||||||
firefoxRelay.generate.mockReturnValue(Promise.resolve("firefoxrelay@example.com"));
|
firefoxRelay.generate.mockResolvedValue("firefoxrelay@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -381,7 +381,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
website: "example.com",
|
website: "example.com",
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, null);
|
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, null);
|
||||||
forwardEmail.generate.mockReturnValue(Promise.resolve("forwardemail@example.com"));
|
forwardEmail.generate.mockResolvedValue("forwardemail@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -414,7 +414,7 @@ describe("LegacyUsernameGenerationService", () => {
|
|||||||
website: "example.com",
|
website: "example.com",
|
||||||
} as UsernameGeneratorOptions;
|
} as UsernameGeneratorOptions;
|
||||||
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, null);
|
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, null);
|
||||||
simpleLogin.generate.mockReturnValue(Promise.resolve("simplelogin@example.com"));
|
simpleLogin.generate.mockResolvedValue("simplelogin@example.com");
|
||||||
const generator = new LegacyUsernameGenerationService(
|
const generator = new LegacyUsernameGenerationService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { zip, firstValueFrom, map, concatMap } from "rxjs";
|
import { zip, firstValueFrom, map, concatMap, combineLatest } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@ -56,7 +56,7 @@ type MappedOptions = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function legacyPasswordGenerationServiceFactory(
|
export function legacyUsernameGenerationServiceFactory(
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
@ -205,10 +205,11 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptions() {
|
getOptions$() {
|
||||||
|
// look upon my works, ye mighty, and despair!
|
||||||
const options$ = this.accountService.activeAccount$.pipe(
|
const options$ = this.accountService.activeAccount$.pipe(
|
||||||
concatMap((account) =>
|
concatMap((account) =>
|
||||||
zip(
|
combineLatest([
|
||||||
this.navigation.options$(account.id),
|
this.navigation.options$(account.id),
|
||||||
this.navigation.defaults$(account.id),
|
this.navigation.defaults$(account.id),
|
||||||
this.catchall.options$(account.id),
|
this.catchall.options$(account.id),
|
||||||
@ -229,7 +230,7 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
|
|||||||
this.forwardEmail.defaults$(account.id),
|
this.forwardEmail.defaults$(account.id),
|
||||||
this.simpleLogin.options$(account.id),
|
this.simpleLogin.options$(account.id),
|
||||||
this.simpleLogin.defaults$(account.id),
|
this.simpleLogin.defaults$(account.id),
|
||||||
),
|
]),
|
||||||
),
|
),
|
||||||
map(
|
map(
|
||||||
([
|
([
|
||||||
@ -273,30 +274,38 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return firstValueFrom(options$);
|
return options$;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions() {
|
||||||
|
return firstValueFrom(this.getOptions$());
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveOptions(options: UsernameGeneratorOptions) {
|
async saveOptions(options: UsernameGeneratorOptions) {
|
||||||
const stored = this.toStoredOptions(options);
|
const stored = this.toStoredOptions(options);
|
||||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a.id)));
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
// generator settings needs to preserve whether password or passphrase is selected,
|
// generator settings needs to preserve whether password or passphrase is selected,
|
||||||
// so `navigationOptions` is mutated.
|
// so `navigationOptions` is mutated.
|
||||||
let navigationOptions = await firstValueFrom(this.navigation.options$(userId));
|
const navigationOptions$ = zip(
|
||||||
|
this.navigation.options$(activeAccount.id),
|
||||||
|
this.navigation.defaults$(activeAccount.id),
|
||||||
|
).pipe(map(([options, defaults]) => options ?? defaults));
|
||||||
|
let navigationOptions = await firstValueFrom(navigationOptions$);
|
||||||
navigationOptions = Object.assign(navigationOptions, stored.generator);
|
navigationOptions = Object.assign(navigationOptions, stored.generator);
|
||||||
await this.navigation.saveOptions(userId, navigationOptions);
|
await this.navigation.saveOptions(activeAccount.id, navigationOptions);
|
||||||
|
|
||||||
// overwrite all other settings with latest values
|
// overwrite all other settings with latest values
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.catchall.saveOptions(userId, stored.algorithms.catchall),
|
this.catchall.saveOptions(activeAccount.id, stored.algorithms.catchall),
|
||||||
this.effUsername.saveOptions(userId, stored.algorithms.effUsername),
|
this.effUsername.saveOptions(activeAccount.id, stored.algorithms.effUsername),
|
||||||
this.subaddress.saveOptions(userId, stored.algorithms.subaddress),
|
this.subaddress.saveOptions(activeAccount.id, stored.algorithms.subaddress),
|
||||||
this.addyIo.saveOptions(userId, stored.forwarders.addyIo),
|
this.addyIo.saveOptions(activeAccount.id, stored.forwarders.addyIo),
|
||||||
this.duckDuckGo.saveOptions(userId, stored.forwarders.duckDuckGo),
|
this.duckDuckGo.saveOptions(activeAccount.id, stored.forwarders.duckDuckGo),
|
||||||
this.fastmail.saveOptions(userId, stored.forwarders.fastmail),
|
this.fastmail.saveOptions(activeAccount.id, stored.forwarders.fastmail),
|
||||||
this.firefoxRelay.saveOptions(userId, stored.forwarders.firefoxRelay),
|
this.firefoxRelay.saveOptions(activeAccount.id, stored.forwarders.firefoxRelay),
|
||||||
this.forwardEmail.saveOptions(userId, stored.forwarders.forwardEmail),
|
this.forwardEmail.saveOptions(activeAccount.id, stored.forwarders.forwardEmail),
|
||||||
this.simpleLogin.saveOptions(userId, stored.forwarders.simpleLogin),
|
this.simpleLogin.saveOptions(activeAccount.id, stored.forwarders.simpleLogin),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { StateProvider } from "../../../platform/state";
|
|||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction";
|
import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction";
|
||||||
import { GENERATOR_SETTINGS } from "../key-definitions";
|
import { GENERATOR_SETTINGS } from "../key-definitions";
|
||||||
import { reduceCollection } from "../reduce-collection.operator";
|
import { distinctIfShallowMatch, reduceCollection } from "../rx-operators";
|
||||||
|
|
||||||
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation";
|
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation";
|
||||||
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
||||||
@ -42,6 +42,7 @@ export class DefaultGeneratorNavigationService implements GeneratorNavigationSer
|
|||||||
evaluator$(userId: UserId) {
|
evaluator$(userId: UserId) {
|
||||||
const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe(
|
const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe(
|
||||||
reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy),
|
reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy),
|
||||||
|
distinctIfShallowMatch(),
|
||||||
map((policy) => new GeneratorNavigationEvaluator(policy)),
|
map((policy) => new GeneratorNavigationEvaluator(policy)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -84,15 +84,6 @@ describe("Password generation strategy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
|
||||||
it("should be a positive non-zero number", () => {
|
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
|
@ -6,7 +6,7 @@ import { StateProvider } from "../../../platform/state";
|
|||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { reduceCollection } from "../reduce-collection.operator";
|
import { distinctIfShallowMatch, reduceCollection } from "../rx-operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
@ -19,8 +19,6 @@ import {
|
|||||||
leastPrivilege,
|
leastPrivilege,
|
||||||
} from "./passphrase-generator-policy";
|
} from "./passphrase-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy} */
|
/** {@link GeneratorStrategy} */
|
||||||
export class PassphraseGeneratorStrategy
|
export class PassphraseGeneratorStrategy
|
||||||
implements GeneratorStrategy<PassphraseGenerationOptions, PassphraseGeneratorPolicy>
|
implements GeneratorStrategy<PassphraseGenerationOptions, PassphraseGeneratorPolicy>
|
||||||
@ -49,15 +47,11 @@ export class PassphraseGeneratorStrategy
|
|||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.cache_ms} */
|
|
||||||
get cache_ms() {
|
|
||||||
return ONE_MINUTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(
|
return pipe(
|
||||||
reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy),
|
reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy),
|
||||||
|
distinctIfShallowMatch(),
|
||||||
map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)),
|
map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { from } from "rxjs";
|
||||||
|
|
||||||
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||||
@ -171,6 +173,10 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
return wordList.join(o.wordSeparator);
|
return wordList.join(o.wordSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOptions$() {
|
||||||
|
return from(this.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
async getOptions(): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
async getOptions(): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
||||||
let options = await this.stateService.getPasswordGenerationOptions();
|
let options = await this.stateService.getPasswordGenerationOptions();
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
@ -336,9 +342,10 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
|||||||
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(userId?: string): Promise<void> {
|
async clear(userId?: string): Promise<GeneratedPasswordHistory[]> {
|
||||||
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
||||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private capitalize(str: string) {
|
private capitalize(str: string) {
|
||||||
|
@ -8,4 +8,4 @@ import { PasswordGenerationOptions } from "./password-generation-options";
|
|||||||
*/
|
*/
|
||||||
export type PasswordGeneratorOptions = PasswordGenerationOptions &
|
export type PasswordGeneratorOptions = PasswordGenerationOptions &
|
||||||
PassphraseGenerationOptions &
|
PassphraseGenerationOptions &
|
||||||
GeneratorNavigation;
|
GeneratorNavigation & { policyUpdated?: boolean };
|
||||||
|
@ -93,15 +93,6 @@ describe("Password generation strategy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
|
||||||
it("should be a positive non-zero number", () => {
|
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
|
@ -6,7 +6,7 @@ import { StateProvider } from "../../../platform/state";
|
|||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction";
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
import { reduceCollection } from "../reduce-collection.operator";
|
import { distinctIfShallowMatch, reduceCollection } from "../rx-operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DefaultPasswordGenerationOptions,
|
DefaultPasswordGenerationOptions,
|
||||||
@ -19,8 +19,6 @@ import {
|
|||||||
leastPrivilege,
|
leastPrivilege,
|
||||||
} from "./password-generator-policy";
|
} from "./password-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy} */
|
/** {@link GeneratorStrategy} */
|
||||||
export class PasswordGeneratorStrategy
|
export class PasswordGeneratorStrategy
|
||||||
implements GeneratorStrategy<PasswordGenerationOptions, PasswordGeneratorPolicy>
|
implements GeneratorStrategy<PasswordGenerationOptions, PasswordGeneratorPolicy>
|
||||||
@ -48,14 +46,11 @@ export class PasswordGeneratorStrategy
|
|||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
get cache_ms() {
|
|
||||||
return ONE_MINUTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(
|
return pipe(
|
||||||
reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy),
|
reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy),
|
||||||
|
distinctIfShallowMatch(),
|
||||||
map((policy) => new PasswordGeneratorOptionsEvaluator(policy)),
|
map((policy) => new PasswordGeneratorOptionsEvaluator(policy)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* include structuredClone in test environment.
|
|
||||||
* @jest-environment ../../../../shared/test.environment.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { of, firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { reduceCollection } from "./reduce-collection.operator";
|
|
||||||
|
|
||||||
describe("reduceCollection", () => {
|
|
||||||
it.each([[null], [undefined], [[]]])(
|
|
||||||
"should return the default value when the collection is %p",
|
|
||||||
async (value: number[]) => {
|
|
||||||
const reduce = (acc: number, value: number) => acc + value;
|
|
||||||
const source$ = of(value);
|
|
||||||
|
|
||||||
const result$ = source$.pipe(reduceCollection(reduce, 100));
|
|
||||||
const result = await firstValueFrom(result$);
|
|
||||||
|
|
||||||
expect(result).toEqual(100);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("should reduce the collection to a single value", async () => {
|
|
||||||
const reduce = (acc: number, value: number) => acc + value;
|
|
||||||
const source$ = of([1, 2, 3]);
|
|
||||||
|
|
||||||
const result$ = source$.pipe(reduceCollection(reduce, 0));
|
|
||||||
const result = await firstValueFrom(result$);
|
|
||||||
|
|
||||||
expect(result).toEqual(6);
|
|
||||||
});
|
|
||||||
});
|
|
87
libs/common/src/tools/generator/rx-operators.spec.ts
Normal file
87
libs/common/src/tools/generator/rx-operators.spec.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* include structuredClone in test environment.
|
||||||
|
* @jest-environment ../../../../shared/test.environment.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { awaitAsync, trackEmissions } from "../../../spec";
|
||||||
|
|
||||||
|
import { distinctIfShallowMatch, reduceCollection } from "./rx-operators";
|
||||||
|
|
||||||
|
describe("reduceCollection", () => {
|
||||||
|
it.each([[null], [undefined], [[]]])(
|
||||||
|
"should return the default value when the collection is %p",
|
||||||
|
async (value: number[]) => {
|
||||||
|
const reduce = (acc: number, value: number) => acc + value;
|
||||||
|
const source$ = of(value);
|
||||||
|
|
||||||
|
const result$ = source$.pipe(reduceCollection(reduce, 100));
|
||||||
|
const result = await firstValueFrom(result$);
|
||||||
|
|
||||||
|
expect(result).toEqual(100);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should reduce the collection to a single value", async () => {
|
||||||
|
const reduce = (acc: number, value: number) => acc + value;
|
||||||
|
const source$ = of([1, 2, 3]);
|
||||||
|
|
||||||
|
const result$ = source$.pipe(reduceCollection(reduce, 0));
|
||||||
|
const result = await firstValueFrom(result$);
|
||||||
|
|
||||||
|
expect(result).toEqual(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("distinctIfShallowMatch", () => {
|
||||||
|
it("emits a single value", async () => {
|
||||||
|
const source$ = of({ foo: true });
|
||||||
|
const pipe$ = source$.pipe(distinctIfShallowMatch());
|
||||||
|
|
||||||
|
const result = trackEmissions(pipe$);
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(result).toEqual([{ foo: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits different values", async () => {
|
||||||
|
const source$ = of({ foo: true }, { foo: false });
|
||||||
|
const pipe$ = source$.pipe(distinctIfShallowMatch());
|
||||||
|
|
||||||
|
const result = trackEmissions(pipe$);
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(result).toEqual([{ foo: true }, { foo: false }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits new keys", async () => {
|
||||||
|
const source$ = of({ foo: true }, { foo: true, bar: true });
|
||||||
|
const pipe$ = source$.pipe(distinctIfShallowMatch());
|
||||||
|
|
||||||
|
const result = trackEmissions(pipe$);
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(result).toEqual([{ foo: true }, { foo: true, bar: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses identical values", async () => {
|
||||||
|
const source$ = of({ foo: true }, { foo: true });
|
||||||
|
const pipe$ = source$.pipe(distinctIfShallowMatch());
|
||||||
|
|
||||||
|
const result = trackEmissions(pipe$);
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(result).toEqual([{ foo: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses removed keys", async () => {
|
||||||
|
const source$ = of({ foo: true, bar: true }, { foo: true });
|
||||||
|
const pipe$ = source$.pipe(distinctIfShallowMatch());
|
||||||
|
|
||||||
|
const result = trackEmissions(pipe$);
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(result).toEqual([{ foo: true, bar: true }]);
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import { map, OperatorFunction } from "rxjs";
|
import { distinctUntilChanged, map, OperatorFunction } from "rxjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable operator that reduces an emitted collection to a single object,
|
* An observable operator that reduces an emitted collection to a single object,
|
||||||
@ -18,3 +18,21 @@ export function reduceCollection<Item, Accumulator>(
|
|||||||
return reduced;
|
return reduced;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable operator that emits distinct values by checking that all
|
||||||
|
* values in the previous entry match the next entry. This method emits
|
||||||
|
* when a key is added and does not when a key is removed.
|
||||||
|
* @remarks This method checks objects. It does not check items in arrays.
|
||||||
|
*/
|
||||||
|
export function distinctIfShallowMatch<Item>(): OperatorFunction<Item, Item> {
|
||||||
|
return distinctUntilChanged((previous, current) => {
|
||||||
|
let isDistinct = true;
|
||||||
|
|
||||||
|
for (const key in current) {
|
||||||
|
isDistinct &&= previous[key] === current[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDistinct;
|
||||||
|
});
|
||||||
|
}
|
@ -59,15 +59,6 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
|
||||||
it("should be a positive non-zero number", () => {
|
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
|
@ -11,8 +11,6 @@ import { NoPolicy } from "../no-policy";
|
|||||||
|
|
||||||
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
/** Strategy for creating usernames using a catchall email address */
|
/** Strategy for creating usernames using a catchall email address */
|
||||||
export class CatchallGeneratorStrategy
|
export class CatchallGeneratorStrategy
|
||||||
implements GeneratorStrategy<CatchallGenerationOptions, NoPolicy>
|
implements GeneratorStrategy<CatchallGenerationOptions, NoPolicy>
|
||||||
@ -42,11 +40,6 @@ export class CatchallGeneratorStrategy
|
|||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.cache_ms} */
|
|
||||||
get cache_ms() {
|
|
||||||
return ONE_MINUTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<CatchallGenerationOptions>()));
|
return pipe(map((_) => new DefaultPolicyEvaluator<CatchallGenerationOptions>()));
|
||||||
|
@ -59,15 +59,6 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
|
||||||
it("should be a positive non-zero number", () => {
|
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
|
@ -14,8 +14,6 @@ import {
|
|||||||
EffUsernameGenerationOptions,
|
EffUsernameGenerationOptions,
|
||||||
} from "./eff-username-generator-options";
|
} from "./eff-username-generator-options";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
/** Strategy for creating usernames from the EFF wordlist */
|
/** Strategy for creating usernames from the EFF wordlist */
|
||||||
export class EffUsernameGeneratorStrategy
|
export class EffUsernameGeneratorStrategy
|
||||||
implements GeneratorStrategy<EffUsernameGenerationOptions, NoPolicy>
|
implements GeneratorStrategy<EffUsernameGenerationOptions, NoPolicy>
|
||||||
@ -45,11 +43,6 @@ export class EffUsernameGeneratorStrategy
|
|||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.cache_ms} */
|
|
||||||
get cache_ms() {
|
|
||||||
return ONE_MINUTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>()));
|
return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>()));
|
||||||
|
@ -10,9 +10,10 @@ import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
|||||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { UserKey } from "../../../types/key";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions";
|
import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../key-definitions";
|
||||||
import { SecretState } from "../state/secret-state";
|
import { BufferedState } from "../state/buffered-state";
|
||||||
|
|
||||||
import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy";
|
||||||
import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go";
|
import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go";
|
||||||
@ -32,6 +33,10 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
|||||||
return DUCK_DUCK_GO_FORWARDER;
|
return DUCK_DUCK_GO_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rolloverKey() {
|
||||||
|
return DUCK_DUCK_GO_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
defaults$ = (userId: UserId) => {
|
defaults$ = (userId: UserId) => {
|
||||||
return of(DefaultDuckDuckGoOptions);
|
return of(DefaultDuckDuckGoOptions);
|
||||||
};
|
};
|
||||||
@ -51,13 +56,22 @@ describe("ForwarderGeneratorStrategy", () => {
|
|||||||
const keyService = mock<CryptoService>();
|
const keyService = mock<CryptoService>();
|
||||||
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
|
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const keyAvailable = of({} as UserKey);
|
||||||
|
keyService.getInMemoryUserKeyFor$.mockReturnValue(keyAvailable);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
it("constructs a secret state", () => {
|
it("constructs a secret state", () => {
|
||||||
const strategy = new TestForwarder(encryptService, keyService, stateProvider);
|
const strategy = new TestForwarder(encryptService, keyService, stateProvider);
|
||||||
|
|
||||||
const result = strategy.durableState(SomeUser);
|
const result = strategy.durableState(SomeUser);
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(SecretState);
|
expect(result).toBeInstanceOf(BufferedState);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the same secret state for a single user", () => {
|
it("returns the same secret state for a single user", () => {
|
||||||
|
@ -8,6 +8,8 @@ import { UserId } from "../../../types/guid";
|
|||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { NoPolicy } from "../no-policy";
|
import { NoPolicy } from "../no-policy";
|
||||||
|
import { BufferedKeyDefinition } from "../state/buffered-key-definition";
|
||||||
|
import { BufferedState } from "../state/buffered-state";
|
||||||
import { PaddedDataPacker } from "../state/padded-data-packer";
|
import { PaddedDataPacker } from "../state/padded-data-packer";
|
||||||
import { SecretClassifier } from "../state/secret-classifier";
|
import { SecretClassifier } from "../state/secret-classifier";
|
||||||
import { SecretKeyDefinition } from "../state/secret-key-definition";
|
import { SecretKeyDefinition } from "../state/secret-key-definition";
|
||||||
@ -16,7 +18,6 @@ import { UserKeyEncryptor } from "../state/user-key-encryptor";
|
|||||||
|
|
||||||
import { ApiOptions } from "./options/forwarder-options";
|
import { ApiOptions } from "./options/forwarder-options";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
|
||||||
const OPTIONS_FRAME_SIZE = 512;
|
const OPTIONS_FRAME_SIZE = 512;
|
||||||
|
|
||||||
/** An email forwarding service configurable through an API. */
|
/** An email forwarding service configurable through an API. */
|
||||||
@ -37,8 +38,6 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
// Uses password generator since there aren't policies
|
// Uses password generator since there aren't policies
|
||||||
// specific to usernames.
|
// specific to usernames.
|
||||||
this.policy = PolicyType.PasswordGenerator;
|
this.policy = PolicyType.PasswordGenerator;
|
||||||
|
|
||||||
this.cache_ms = ONE_MINUTE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private durableStates = new Map<UserId, SingleUserState<Options>>();
|
private durableStates = new Map<UserId, SingleUserState<Options>>();
|
||||||
@ -48,7 +47,19 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
let state = this.durableStates.get(userId);
|
let state = this.durableStates.get(userId);
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
const encryptor = this.createEncryptor();
|
state = this.createState(userId);
|
||||||
|
|
||||||
|
this.durableStates.set(userId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
private createState(userId: UserId): SingleUserState<Options> {
|
||||||
|
// construct the encryptor
|
||||||
|
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
||||||
|
const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
||||||
|
|
||||||
// always exclude request properties
|
// always exclude request properties
|
||||||
const classifier = SecretClassifier.allSecret<Options>().exclude("website");
|
const classifier = SecretClassifier.allSecret<Options>().exclude("website");
|
||||||
|
|
||||||
@ -60,7 +71,7 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
});
|
});
|
||||||
|
|
||||||
// the type parameter is explicit because type inference fails for `Omit<Options, "website">`
|
// the type parameter is explicit because type inference fails for `Omit<Options, "website">`
|
||||||
state = SecretState.from<
|
const secretState = SecretState.from<
|
||||||
Options,
|
Options,
|
||||||
void,
|
void,
|
||||||
Options,
|
Options,
|
||||||
@ -68,16 +79,18 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
Omit<Options, "website">
|
Omit<Options, "website">
|
||||||
>(userId, key, this.stateProvider, encryptor);
|
>(userId, key, this.stateProvider, encryptor);
|
||||||
|
|
||||||
this.durableStates.set(userId, state);
|
// rollover should occur once the user key is available for decryption
|
||||||
}
|
const canDecrypt$ = this.keyService
|
||||||
|
.getInMemoryUserKeyFor$(userId)
|
||||||
|
.pipe(map((key) => key !== null));
|
||||||
|
const rolloverState = new BufferedState(
|
||||||
|
this.stateProvider,
|
||||||
|
this.rolloverKey,
|
||||||
|
secretState,
|
||||||
|
canDecrypt$,
|
||||||
|
);
|
||||||
|
|
||||||
return state;
|
return rolloverState;
|
||||||
};
|
|
||||||
|
|
||||||
private createEncryptor() {
|
|
||||||
// construct the encryptor
|
|
||||||
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
|
||||||
return new UserKeyEncryptor(this.encryptService, this.keyService, packer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets the default options. */
|
/** Gets the default options. */
|
||||||
@ -86,6 +99,9 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
/** Determine where forwarder configuration is stored */
|
/** Determine where forwarder configuration is stored */
|
||||||
protected abstract readonly key: UserKeyDefinition<Options>;
|
protected abstract readonly key: UserKeyDefinition<Options>;
|
||||||
|
|
||||||
|
/** Determine where forwarder rollover configuration is stored */
|
||||||
|
protected abstract readonly rolloverKey: BufferedKeyDefinition<Options, Options>;
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
toEvaluator = () => {
|
toEvaluator = () => {
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<Options>()));
|
return pipe(map((_) => new DefaultPolicyEvaluator<Options>()));
|
||||||
|
@ -6,7 +6,7 @@ import { EncryptService } from "../../../../platform/abstractions/encrypt.servic
|
|||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
import { UserId } from "../../../../types/guid";
|
||||||
import { ADDY_IO_FORWARDER } from "../../key-definitions";
|
import { ADDY_IO_FORWARDER, ADDY_IO_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options";
|
import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options";
|
||||||
@ -44,6 +44,11 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy<
|
|||||||
return ADDY_IO_FORWARDER;
|
return ADDY_IO_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||||
|
get rolloverKey() {
|
||||||
|
return ADDY_IO_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
defaults$ = (userId: UserId) => {
|
defaults$ = (userId: UserId) => {
|
||||||
return new BehaviorSubject({ ...DefaultAddyIoOptions });
|
return new BehaviorSubject({ ...DefaultAddyIoOptions });
|
||||||
|
@ -6,7 +6,7 @@ import { EncryptService } from "../../../../platform/abstractions/encrypt.servic
|
|||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
import { UserId } from "../../../../types/guid";
|
||||||
import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions";
|
import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { ApiOptions } from "../options/forwarder-options";
|
import { ApiOptions } from "../options/forwarder-options";
|
||||||
@ -40,6 +40,11 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions>
|
|||||||
return DUCK_DUCK_GO_FORWARDER;
|
return DUCK_DUCK_GO_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||||
|
get rolloverKey() {
|
||||||
|
return DUCK_DUCK_GO_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
defaults$ = (userId: UserId) => {
|
defaults$ = (userId: UserId) => {
|
||||||
return new BehaviorSubject({ ...DefaultDuckDuckGoOptions });
|
return new BehaviorSubject({ ...DefaultDuckDuckGoOptions });
|
||||||
|
@ -6,7 +6,7 @@ import { EncryptService } from "../../../../platform/abstractions/encrypt.servic
|
|||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FASTMAIL_FORWARDER } from "../../key-definitions";
|
import { FASTMAIL_FORWARDER, FASTMAIL_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options";
|
import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options";
|
||||||
@ -47,6 +47,11 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E
|
|||||||
return new BehaviorSubject({ ...DefaultFastmailOptions });
|
return new BehaviorSubject({ ...DefaultFastmailOptions });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||||
|
get rolloverKey() {
|
||||||
|
return FASTMAIL_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
generate = async (options: ApiOptions & EmailPrefixOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
|
@ -6,7 +6,7 @@ import { EncryptService } from "../../../../platform/abstractions/encrypt.servic
|
|||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions";
|
import { FIREFOX_RELAY_FORWARDER, FIREFOX_RELAY_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { ApiOptions } from "../options/forwarder-options";
|
import { ApiOptions } from "../options/forwarder-options";
|
||||||
@ -40,6 +40,11 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions
|
|||||||
return FIREFOX_RELAY_FORWARDER;
|
return FIREFOX_RELAY_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||||
|
get rolloverKey() {
|
||||||
|
return FIREFOX_RELAY_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
defaults$ = (userId: UserId) => {
|
defaults$ = (userId: UserId) => {
|
||||||
return new BehaviorSubject({ ...DefaultFirefoxRelayOptions });
|
return new BehaviorSubject({ ...DefaultFirefoxRelayOptions });
|
||||||
|
@ -7,7 +7,7 @@ import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
|||||||
import { Utils } from "../../../../platform/misc/utils";
|
import { Utils } from "../../../../platform/misc/utils";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
import { UserId } from "../../../../types/guid";
|
||||||
import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions";
|
import { FORWARD_EMAIL_FORWARDER, FORWARD_EMAIL_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options";
|
import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options";
|
||||||
@ -49,6 +49,11 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy<
|
|||||||
return new BehaviorSubject({ ...DefaultForwardEmailOptions });
|
return new BehaviorSubject({ ...DefaultForwardEmailOptions });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||||
|
get rolloverKey() {
|
||||||
|
return FORWARD_EMAIL_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.generate} */
|
/** {@link ForwarderGeneratorStrategy.generate} */
|
||||||
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
generate = async (options: ApiOptions & EmailDomainOptions) => {
|
||||||
if (!options.token || options.token === "") {
|
if (!options.token || options.token === "") {
|
||||||
|
@ -6,7 +6,7 @@ import { EncryptService } from "../../../../platform/abstractions/encrypt.servic
|
|||||||
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../../platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "../../../../platform/state";
|
import { StateProvider } from "../../../../platform/state";
|
||||||
import { UserId } from "../../../../types/guid";
|
import { UserId } from "../../../../types/guid";
|
||||||
import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions";
|
import { SIMPLE_LOGIN_FORWARDER, SIMPLE_LOGIN_BUFFER } from "../../key-definitions";
|
||||||
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy";
|
||||||
import { Forwarders } from "../options/constants";
|
import { Forwarders } from "../options/constants";
|
||||||
import { SelfHostedApiOptions } from "../options/forwarder-options";
|
import { SelfHostedApiOptions } from "../options/forwarder-options";
|
||||||
@ -41,6 +41,11 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA
|
|||||||
return SIMPLE_LOGIN_FORWARDER;
|
return SIMPLE_LOGIN_FORWARDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link ForwarderGeneratorStrategy.rolloverKey} */
|
||||||
|
get rolloverKey() {
|
||||||
|
return SIMPLE_LOGIN_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
/** {@link ForwarderGeneratorStrategy.defaults$} */
|
||||||
defaults$ = (userId: UserId) => {
|
defaults$ = (userId: UserId) => {
|
||||||
return new BehaviorSubject({ ...DefaultSimpleLoginOptions });
|
return new BehaviorSubject({ ...DefaultSimpleLoginOptions });
|
||||||
|
@ -62,15 +62,6 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
|
||||||
it("should be a positive non-zero number", () => {
|
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
|
@ -14,8 +14,6 @@ import {
|
|||||||
SubaddressGenerationOptions,
|
SubaddressGenerationOptions,
|
||||||
} from "./subaddress-generator-options";
|
} from "./subaddress-generator-options";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
|
||||||
|
|
||||||
/** Strategy for creating an email subaddress
|
/** Strategy for creating an email subaddress
|
||||||
* @remarks The subaddress is the part following the `+`.
|
* @remarks The subaddress is the part following the `+`.
|
||||||
* For example, if the email address is `jd+xyz@domain.io`,
|
* For example, if the email address is `jd+xyz@domain.io`,
|
||||||
@ -49,11 +47,6 @@ export class SubaddressGeneratorStrategy
|
|||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.cache_ms} */
|
|
||||||
get cache_ms() {
|
|
||||||
return ONE_MINUTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.toEvaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
toEvaluator() {
|
toEvaluator() {
|
||||||
return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>()));
|
return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>()));
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { from } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../abstractions/api.service";
|
import { ApiService } from "../../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
@ -158,6 +160,10 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr
|
|||||||
return forwarder.generate(this.apiService, forwarderOptions);
|
return forwarder.generate(this.apiService, forwarderOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOptions$() {
|
||||||
|
return from(this.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
async getOptions(): Promise<UsernameGeneratorOptions> {
|
async getOptions(): Promise<UsernameGeneratorOptions> {
|
||||||
let options = await this.stateService.getUsernameGenerationOptions();
|
let options = await this.stateService.getUsernameGenerationOptions();
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
|
Loading…
Reference in New Issue
Block a user