Skip to content

Commit

Permalink
fix legacy forwarder loading logic
Browse files Browse the repository at this point in the history
  • Loading branch information
audreyality committed Nov 27, 2024
1 parent 0406eae commit 312ebee
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 58 deletions.
25 changes: 24 additions & 1 deletion libs/common/src/tools/state/object-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import type { StateDefinition } from "../../platform/state/state-definition";
import { ClassifiedFormat } from "./classified-format";
import { Classifier } from "./classifier";

/** Determines the format of persistent storage.
* `plain` storage is a plain-old javascript object. Use this type
* when you are performing your own encryption and decryption.
* `classified` uses the `ClassifiedFormat` type as its format.
* `secret-state` uses `Array<ClassifiedFormat>` with a length of 1.
* @remarks - CAUTION! If your on-disk data is not in a correct format,
* the storage system treats the data as corrupt and returns your initial
* value.
*/
export type ObjectStorageFormat = "plain" | "classified" | "secret-state";

/** A key for storing JavaScript objects (`{ an: "example" }`)
* in a UserStateSubject.
*/
Expand All @@ -20,7 +31,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
key: string;
state: StateDefinition;
classifier: Classifier<State, Disclosed, Secret>;
format: "plain" | "classified";
format: ObjectStorageFormat;
options: UserKeyDefinitionOptions<State>;
initial?: State;
};
Expand All @@ -47,6 +58,18 @@ export function toUserKeyDefinition<State, Secret, Disclosed>(
},
);

return classified;
} else if (key.format === "secret-state") {
const classified = new UserKeyDefinition<[ClassifiedFormat<void, Disclosed>]>(

Check warning on line 63 in libs/common/src/tools/state/object-key.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/object-key.ts#L63

Added line #L63 was not covered by tests
key.state,
key.key,
{
cleanupDelayMs: key.options.cleanupDelayMs,
deserializer: (jsonValue) => jsonValue as [ClassifiedFormat<void, Disclosed>],

Check warning on line 68 in libs/common/src/tools/state/object-key.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/object-key.ts#L68

Added line #L68 was not covered by tests
clearOn: key.options.clearOn,
},
);

return classified;
} else {
throw new Error(`unknown format: ${key.format}`);
Expand Down
137 changes: 86 additions & 51 deletions libs/common/src/tools/state/user-state-subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,36 +304,63 @@ export class UserStateSubject<
return (input$) => input$ as Observable<State>;
}

// if the key supports encryption, enable encryptor support
// all other keys support encryption; enable encryptor support
return pipe(
this.mapToClassifiedFormat(),
combineLatestWith(encryptor$),
concatMap(async ([input, encryptor]) => {
// pass through null values
if (input === null || input === undefined) {
return null;

Check warning on line 314 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L314

Added line #L314 was not covered by tests
}

// decrypt classified data
const { secret, disclosed } = input;
const encrypted = EncString.fromJSON(secret);
const decryptedSecret = await encryptor.decrypt<Secret>(encrypted);

// assemble into proper state
const declassified = this.objectKey.classifier.declassify(disclosed, decryptedSecret);
const state = this.objectKey.options.deserializer(declassified);

return state;
}),
);
}

private mapToClassifiedFormat(): OperatorFunction<unknown, ClassifiedFormat<unknown, unknown>> {
// FIXME: warn when data is dropped in the console and/or report an error
// through the observable; consider redirecting dropped data to a recovery
// location

// user-state subject's default format is object-aware
if (this.objectKey && this.objectKey.format === "classified") {
return pipe(
combineLatestWith(encryptor$),
concatMap(async ([input, encryptor]) => {
// pass through null values
if (input === null || input === undefined) {
return null;
}
return map((input) => {
if (!isClassifiedFormat(input)) {
return null;

Check warning on line 340 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L340

Added line #L340 was not covered by tests
}

// fail fast if the format is incorrect
if (!isClassifiedFormat(input)) {
throw new Error(`Cannot declassify ${this.key.key}; unknown format.`);
}
return input;
});
}

// decrypt classified data
const { secret, disclosed } = input;
const encrypted = EncString.fromJSON(secret);
const decryptedSecret = await encryptor.decrypt<Secret>(encrypted);
// secret state's format wraps objects in an array
if (this.objectKey && this.objectKey.format === "secret-state") {
return map((input) => {

Check warning on line 349 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L349

Added line #L349 was not covered by tests
if (!Array.isArray(input)) {
return null;

Check warning on line 351 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L351

Added line #L351 was not covered by tests
}

// assemble into proper state
const declassified = this.objectKey.classifier.declassify(disclosed, decryptedSecret);
const state = this.objectKey.options.deserializer(declassified);
const [unwrapped] = input;

Check warning on line 354 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L354

Added line #L354 was not covered by tests
if (!isClassifiedFormat(unwrapped)) {
return null;

Check warning on line 356 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L356

Added line #L356 was not covered by tests
}

return state;
}),
);
return unwrapped;

Check warning on line 359 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L359

Added line #L359 was not covered by tests
});
}

throw new Error(`unknown serialization format: ${this.objectKey.format}`);
throw new Error(`unsupported serialization format: ${this.objectKey.format}`);

Check warning on line 363 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L363

Added line #L363 was not covered by tests
}

private classify(encryptor$: Observable<UserEncryptor>): OperatorFunction<State, unknown> {
Expand All @@ -346,41 +373,49 @@ export class UserStateSubject<
);
}

// if the key supports encryption, enable encryptor support
if (this.objectKey && this.objectKey.format === "classified") {
return pipe(
withLatestReady(encryptor$),
concatMap(async ([input, encryptor]) => {
// fail fast if there's no value
if (input === null || input === undefined) {
return null;
}
// all other keys support encryption; enable encryptor support
return pipe(
withLatestReady(encryptor$),
concatMap(async ([input, encryptor]) => {
// fail fast if there's no value
if (input === null || input === undefined) {
return null;

Check warning on line 382 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L382

Added line #L382 was not covered by tests
}

// split data by classification level
const serialized = JSON.parse(JSON.stringify(input));
const classified = this.objectKey.classifier.classify(serialized);
// split data by classification level
const serialized = JSON.parse(JSON.stringify(input));
const classified = this.objectKey.classifier.classify(serialized);

// protect data
const encrypted = await encryptor.encrypt(classified.secret);
const secret = JSON.parse(JSON.stringify(encrypted));
// protect data
const encrypted = await encryptor.encrypt(classified.secret);
const secret = JSON.parse(JSON.stringify(encrypted));

// wrap result in classified format envelope for storage
const envelope = {
id: null as void,
secret,
disclosed: classified.disclosed,
} satisfies ClassifiedFormat<void, Disclosed>;
// wrap result in classified format envelope for storage
const envelope = {
id: null as void,
secret,
disclosed: classified.disclosed,
} satisfies ClassifiedFormat<void, Disclosed>;

// deliberate type erasure; the type is restored during `declassify`
return envelope as unknown;
}),
);
// deliberate type erasure; the type is restored during `declassify`
return envelope as ClassifiedFormat<unknown, unknown>;
}),
this.mapToStorageFormat(),
);
}

private mapToStorageFormat(): OperatorFunction<ClassifiedFormat<unknown, unknown>, unknown> {
// user-state subject's default format is object-aware
if (this.objectKey && this.objectKey.format === "classified") {
return map((input) => input as unknown);
}

// FIXME: add "encrypted" format --> key contains encryption logic
// CONSIDER: should "classified format" algorithm be embedded in subject keys...?
// secret state's format wraps objects in an array
if (this.objectKey && this.objectKey.format === "secret-state") {
return map((input) => [input] as unknown);

Check warning on line 415 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L415

Added line #L415 was not covered by tests
}

throw new Error(`unknown serialization format: ${this.objectKey.format}`);
throw new Error(`unsupported serialization format: ${this.objectKey.format}`);

Check warning on line 418 in libs/common/src/tools/state/user-state-subject.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/tools/state/user-state-subject.ts#L418

Added line #L418 was not covered by tests
}

/** The userId to which the subject is bound.
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/generator/core/src/integration/addy-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const forwarder = Object.freeze({
// e.g. key: "forwarder.AddyIo.local.settings",
key: "addyIoForwarder",
target: "object",
format: "classified",
format: "secret-state",
classifier: new PrivateClassifier<AddyIoSettings>(),
state: GENERATOR_DISK,
initial: defaultSettings,
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/generator/core/src/integration/duck-duck-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const forwarder = Object.freeze({
// e.g. key: "forwarder.DuckDuckGo.local.settings",
key: "duckDuckGoForwarder",
target: "object",
format: "classified",
format: "secret-state",
classifier: new PrivateClassifier<DuckDuckGoSettings>(),
state: GENERATOR_DISK,
initial: defaultSettings,
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/generator/core/src/integration/fastmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const forwarder = Object.freeze({
// e.g. key: "forwarder.Fastmail.local.settings"
key: "fastmailForwarder",
target: "object",
format: "classified",
format: "secret-state",
classifier: new PrivateClassifier<FastmailSettings>(),
state: GENERATOR_DISK,
initial: defaultSettings,
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/generator/core/src/integration/firefox-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const forwarder = Object.freeze({
// e.g. key: "forwarder.Firefox.local.settings",
key: "firefoxRelayForwarder",
target: "object",
format: "classified",
format: "secret-state",
classifier: new PrivateClassifier<FirefoxRelaySettings>(),
state: GENERATOR_DISK,
initial: defaultSettings,
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/generator/core/src/integration/forward-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const forwarder = Object.freeze({
// e.g. key: "forwarder.ForwardEmail.local.settings",
key: "forwardEmailForwarder",
target: "object",
format: "classified",
format: "secret-state",
classifier: new PrivateClassifier<ForwardEmailSettings>(),
state: GENERATOR_DISK,
initial: defaultSettings,
Expand Down
2 changes: 1 addition & 1 deletion libs/tools/generator/core/src/integration/simple-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const forwarder = Object.freeze({
// e.g. key: "forwarder.SimpleLogin.local.settings",
key: "simpleLoginForwarder",
target: "object",
format: "classified",
format: "secret-state",
classifier: new PrivateClassifier<SimpleLoginSettings>(),
state: GENERATOR_DISK,
initial: defaultSettings,
Expand Down

0 comments on commit 312ebee

Please sign in to comment.