Tools/specify-clearon-conditions (#8596)

* Specify user clear events for event upload

* Specify generator clear events

* Specify clear events for user send data

* Specify generic clear on logout for encrypted secret state

* Allow `clearOn`event to be passed into secret state

* Match current data persistence rules

* Clear ui memory on lock + logout
This commit is contained in:
Matt Gibson 2024-04-08 07:26:22 -05:00 committed by GitHub
parent 759e48728e
commit 1308b326fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 64 additions and 35 deletions

View File

@ -8,7 +8,7 @@ export { ActiveUserState, SingleUserState, CombinedState } from "./user-state";
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider"; export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
export { KeyDefinition, KeyDefinitionOptions } from "./key-definition"; export { KeyDefinition, KeyDefinitionOptions } from "./key-definition";
export { StateUpdateOptions } from "./state-update-options"; export { StateUpdateOptions } from "./state-update-options";
export { UserKeyDefinition } from "./user-key-definition"; export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition";
export { StateEventRunnerService } from "./state-event-runner.service"; export { StateEventRunnerService } from "./state-event-runner.service";
export * from "./state-definitions"; export * from "./state-definitions";

View File

@ -8,7 +8,7 @@ import { StateDefinition } from "./state-definition";
export type ClearEvent = "lock" | "logout"; export type ClearEvent = "lock" | "logout";
type UserKeyDefinitionOptions<T> = KeyDefinitionOptions<T> & { export type UserKeyDefinitionOptions<T> = KeyDefinitionOptions<T> & {
clearOn: ClearEvent[]; clearOn: ClearEvent[];
}; };

View File

@ -1,10 +1,11 @@
import { EventData } from "../../models/data/event.data"; import { EventData } from "../../models/data/event.data";
import { KeyDefinition, EVENT_COLLECTION_DISK } from "../../platform/state"; import { EVENT_COLLECTION_DISK, UserKeyDefinition } from "../../platform/state";
export const EVENT_COLLECTION: KeyDefinition<EventData[]> = KeyDefinition.array<EventData>( export const EVENT_COLLECTION = UserKeyDefinition.array<EventData>(
EVENT_COLLECTION_DISK, EVENT_COLLECTION_DISK,
"events", "events",
{ {
deserializer: (s) => EventData.fromJSON(s), deserializer: (s) => EventData.fromJSON(s),
clearOn: ["logout"],
}, },
); );

View File

@ -1,4 +1,4 @@
import { GENERATOR_DISK, GENERATOR_MEMORY, KeyDefinition } from "../../platform/state"; import { GENERATOR_DISK, GENERATOR_MEMORY, UserKeyDefinition } from "../../platform/state";
import { GeneratedCredential } from "./history/generated-credential"; import { GeneratedCredential } from "./history/generated-credential";
import { GeneratorNavigation } from "./navigation/generator-navigation"; import { GeneratorNavigation } from "./navigation/generator-navigation";
@ -17,110 +17,122 @@ import {
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";
/** plaintext password generation options */ /** plaintext password generation options */
export const GENERATOR_SETTINGS = new KeyDefinition<GeneratorNavigation>( export const GENERATOR_SETTINGS = new UserKeyDefinition<GeneratorNavigation>(
GENERATOR_MEMORY, GENERATOR_MEMORY,
"generatorSettings", "generatorSettings",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: ["lock", "logout"],
}, },
); );
/** plaintext password generation options */ /** plaintext password generation options */
export const PASSWORD_SETTINGS = new KeyDefinition<PasswordGenerationOptions>( export const PASSWORD_SETTINGS = new UserKeyDefinition<PasswordGenerationOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"passwordGeneratorSettings", "passwordGeneratorSettings",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** plaintext passphrase generation options */ /** plaintext passphrase generation options */
export const PASSPHRASE_SETTINGS = new KeyDefinition<PassphraseGenerationOptions>( export const PASSPHRASE_SETTINGS = new UserKeyDefinition<PassphraseGenerationOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"passphraseGeneratorSettings", "passphraseGeneratorSettings",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** plaintext username generation options */ /** plaintext username generation options */
export const EFF_USERNAME_SETTINGS = new KeyDefinition<EffUsernameGenerationOptions>( export const EFF_USERNAME_SETTINGS = new UserKeyDefinition<EffUsernameGenerationOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"effUsernameGeneratorSettings", "effUsernameGeneratorSettings",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** plaintext configuration for a domain catch-all address. */ /** plaintext configuration for a domain catch-all address. */
export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>( export const CATCHALL_SETTINGS = new UserKeyDefinition<CatchallGenerationOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"catchallGeneratorSettings", "catchallGeneratorSettings",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** plaintext configuration for an email subaddress. */ /** plaintext configuration for an email subaddress. */
export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions>( export const SUBADDRESS_SETTINGS = new UserKeyDefinition<SubaddressGenerationOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"subaddressGeneratorSettings", "subaddressGeneratorSettings",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** backing store configuration for {@link Forwarders.AddyIo} */ /** backing store configuration for {@link Forwarders.AddyIo} */
export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>( export const ADDY_IO_FORWARDER = new UserKeyDefinition<SelfHostedApiOptions & EmailDomainOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"addyIoForwarder", "addyIoForwarder",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** backing store configuration for {@link Forwarders.DuckDuckGo} */ /** backing store configuration for {@link Forwarders.DuckDuckGo} */
export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>( export const DUCK_DUCK_GO_FORWARDER = new UserKeyDefinition<ApiOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"duckDuckGoForwarder", "duckDuckGoForwarder",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** backing store configuration for {@link Forwarders.FastMail} */ /** backing store configuration for {@link Forwarders.FastMail} */
export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>( export const FASTMAIL_FORWARDER = new UserKeyDefinition<ApiOptions & EmailPrefixOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"fastmailForwarder", "fastmailForwarder",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** backing store configuration for {@link Forwarders.FireFoxRelay} */ /** backing store configuration for {@link Forwarders.FireFoxRelay} */
export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>( export const FIREFOX_RELAY_FORWARDER = new UserKeyDefinition<ApiOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"firefoxRelayForwarder", "firefoxRelayForwarder",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** backing store configuration for {@link Forwarders.ForwardEmail} */ /** backing store configuration for {@link Forwarders.ForwardEmail} */
export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>( export const FORWARD_EMAIL_FORWARDER = new UserKeyDefinition<ApiOptions & EmailDomainOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"forwardEmailForwarder", "forwardEmailForwarder",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
/** backing store configuration for {@link forwarders.SimpleLogin} */ /** backing store configuration for {@link forwarders.SimpleLogin} */
export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>( export const SIMPLE_LOGIN_FORWARDER = new UserKeyDefinition<SelfHostedApiOptions>(
GENERATOR_DISK, GENERATOR_DISK,
"simpleLoginForwarder", "simpleLoginForwarder",
{ {
deserializer: (value) => value, deserializer: (value) => value,
clearOn: [],
}, },
); );
@ -131,5 +143,6 @@ export const GENERATOR_HISTORY = SecretKeyDefinition.array(
SecretClassifier.allSecret<GeneratedCredential>(), SecretClassifier.allSecret<GeneratedCredential>(),
{ {
deserializer: GeneratedCredential.fromJSON, deserializer: GeneratedCredential.fromJSON,
clearOn: ["logout"],
}, },
); );

View File

@ -1,16 +1,17 @@
import { GENERATOR_DISK } from "../../../platform/state"; import { GENERATOR_DISK, UserKeyDefinitionOptions } from "../../../platform/state";
import { SecretClassifier } from "./secret-classifier"; import { SecretClassifier } from "./secret-classifier";
import { SecretKeyDefinition } from "./secret-key-definition"; import { SecretKeyDefinition } from "./secret-key-definition";
describe("SecretKeyDefinition", () => { describe("SecretKeyDefinition", () => {
const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); const classifier = SecretClassifier.allSecret<{ foo: boolean }>();
const options = { deserializer: (v: any) => v }; const options: UserKeyDefinitionOptions<any> = { deserializer: (v: any) => v, clearOn: [] };
it("toEncryptedStateKey returns a key", () => { it("toEncryptedStateKey returns a key", () => {
const expectedOptions = { const expectedOptions: UserKeyDefinitionOptions<any> = {
deserializer: (v: any) => v, deserializer: (v: any) => v,
cleanupDelayMs: 100, cleanupDelayMs: 100,
clearOn: ["logout", "lock"],
}; };
const definition = SecretKeyDefinition.value( const definition = SecretKeyDefinition.value(
GENERATOR_DISK, GENERATOR_DISK,
@ -26,6 +27,7 @@ describe("SecretKeyDefinition", () => {
expect(result.stateDefinition).toEqual(GENERATOR_DISK); expect(result.stateDefinition).toEqual(GENERATOR_DISK);
expect(result.key).toBe("key"); expect(result.key).toBe("key");
expect(result.cleanupDelayMs).toBe(expectedOptions.cleanupDelayMs); expect(result.cleanupDelayMs).toBe(expectedOptions.cleanupDelayMs);
expect(result.clearOn).toEqual(expectedOptions.clearOn);
expect(deserializerResult).toBe(expectedDeserializerResult); expect(deserializerResult).toBe(expectedDeserializerResult);
}); });

View File

@ -1,4 +1,4 @@
import { KeyDefinition, KeyDefinitionOptions } from "../../../platform/state"; import { UserKeyDefinitionOptions, UserKeyDefinition } from "../../../platform/state";
// eslint-disable-next-line -- `StateDefinition` used as an argument // eslint-disable-next-line -- `StateDefinition` used as an argument
import { StateDefinition } from "../../../platform/state/state-definition"; import { StateDefinition } from "../../../platform/state/state-definition";
import { ClassifiedFormat } from "./classified-format"; import { ClassifiedFormat } from "./classified-format";
@ -11,7 +11,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec
readonly stateDefinition: StateDefinition, readonly stateDefinition: StateDefinition,
readonly key: string, readonly key: string,
readonly classifier: SecretClassifier<Inner, Disclosed, Secret>, readonly classifier: SecretClassifier<Inner, Disclosed, Secret>,
readonly options: KeyDefinitionOptions<Inner>, readonly options: UserKeyDefinitionOptions<Inner>,
// type erasure is necessary here because typescript doesn't support // type erasure is necessary here because typescript doesn't support
// higher kinded types that generalize over collections. The invariants // higher kinded types that generalize over collections. The invariants
// needed to make this typesafe are maintained by the static factories. // needed to make this typesafe are maintained by the static factories.
@ -21,12 +21,14 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec
/** Converts the secret key to the `KeyDefinition` used for secret storage. */ /** Converts the secret key to the `KeyDefinition` used for secret storage. */
toEncryptedStateKey() { toEncryptedStateKey() {
const secretKey = new KeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( const secretKey = new UserKeyDefinition<ClassifiedFormat<Id, Disclosed>[]>(
this.stateDefinition, this.stateDefinition,
this.key, this.key,
{ {
cleanupDelayMs: this.options.cleanupDelayMs, cleanupDelayMs: this.options.cleanupDelayMs,
deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[], deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[],
// Clear encrypted state on logout
clearOn: this.options.clearOn,
}, },
); );
@ -45,7 +47,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec
stateDefinition: StateDefinition, stateDefinition: StateDefinition,
key: string, key: string,
classifier: SecretClassifier<Value, Disclosed, Secret>, classifier: SecretClassifier<Value, Disclosed, Secret>,
options: KeyDefinitionOptions<Value>, options: UserKeyDefinitionOptions<Value>,
) { ) {
return new SecretKeyDefinition<Value, void, Value, Disclosed, Secret>( return new SecretKeyDefinition<Value, void, Value, Disclosed, Secret>(
stateDefinition, stateDefinition,
@ -69,7 +71,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec
stateDefinition: StateDefinition, stateDefinition: StateDefinition,
key: string, key: string,
classifier: SecretClassifier<Item, Disclosed, Secret>, classifier: SecretClassifier<Item, Disclosed, Secret>,
options: KeyDefinitionOptions<Item>, options: UserKeyDefinitionOptions<Item>,
) { ) {
return new SecretKeyDefinition<Item[], number, Item, Disclosed, Secret>( return new SecretKeyDefinition<Item[], number, Item, Disclosed, Secret>(
stateDefinition, stateDefinition,
@ -93,7 +95,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec
stateDefinition: StateDefinition, stateDefinition: StateDefinition,
key: string, key: string,
classifier: SecretClassifier<Item, Disclosed, Secret>, classifier: SecretClassifier<Item, Disclosed, Secret>,
options: KeyDefinitionOptions<Item>, options: UserKeyDefinitionOptions<Item>,
) { ) {
return new SecretKeyDefinition<Record<Id, Item>, Id, Item, Disclosed, Secret>( return new SecretKeyDefinition<Record<Id, Item>, Id, Item, Disclosed, Secret>(
stateDefinition, stateDefinition,

View File

@ -3,7 +3,7 @@ import { Observable, map, pipe } from "rxjs";
import { PolicyType } from "../../../admin-console/enums"; import { PolicyType } from "../../../admin-console/enums";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state"; import { SingleUserState, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid"; 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";
@ -56,6 +56,7 @@ export abstract class ForwarderGeneratorStrategy<
const key = SecretKeyDefinition.value(this.key.stateDefinition, this.key.key, classifier, { const key = SecretKeyDefinition.value(this.key.stateDefinition, this.key.key, classifier, {
deserializer: (d) => this.key.deserializer(d), deserializer: (d) => this.key.deserializer(d),
cleanupDelayMs: this.key.cleanupDelayMs, cleanupDelayMs: this.key.cleanupDelayMs,
clearOn: this.key.clearOn,
}); });
// 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">`
@ -83,7 +84,7 @@ export abstract class ForwarderGeneratorStrategy<
abstract defaults$: (userId: UserId) => Observable<Options>; abstract defaults$: (userId: UserId) => Observable<Options>;
/** Determine where forwarder configuration is stored */ /** Determine where forwarder configuration is stored */
protected abstract readonly key: KeyDefinition<Options>; protected abstract readonly key: UserKeyDefinition<Options>;
/** {@link GeneratorStrategy.toEvaluator} */ /** {@link GeneratorStrategy.toEvaluator} */
toEvaluator = () => { toEvaluator = () => {

View File

@ -1,13 +1,23 @@
import { KeyDefinition, SEND_DISK, SEND_MEMORY } from "../../../platform/state"; import { SEND_DISK, SEND_MEMORY, UserKeyDefinition } from "../../../platform/state";
import { SendData } from "../models/data/send.data"; import { SendData } from "../models/data/send.data";
import { SendView } from "../models/view/send.view"; import { SendView } from "../models/view/send.view";
/** Encrypted send state stored on disk */ /** Encrypted send state stored on disk */
export const SEND_USER_ENCRYPTED = KeyDefinition.record<SendData>(SEND_DISK, "sendUserEncrypted", { export const SEND_USER_ENCRYPTED = UserKeyDefinition.record<SendData>(
deserializer: (obj: SendData) => obj, SEND_DISK,
}); "sendUserEncrypted",
{
deserializer: (obj: SendData) => obj,
clearOn: ["logout"],
},
);
/** Decrypted send state stored in memory */ /** Decrypted send state stored in memory */
export const SEND_USER_DECRYPTED = new KeyDefinition<SendView[]>(SEND_MEMORY, "sendUserDecrypted", { export const SEND_USER_DECRYPTED = new UserKeyDefinition<SendView[]>(
deserializer: (obj) => obj, SEND_MEMORY,
}); "sendUserDecrypted",
{
deserializer: (obj) => obj,
clearOn: ["lock"],
},
);