mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
[PM-15061] extract encryptors from generator service (#12068)
* introduce legacy encryptor provider * port credential generation service to encryptor provider
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>]>(
|
||||
key.state,
|
||||
key.key,
|
||||
{
|
||||
cleanupDelayMs: key.options.cleanupDelayMs,
|
||||
deserializer: (jsonValue) => jsonValue as [ClassifiedFormat<void, Disclosed>],
|
||||
clearOn: key.options.clearOn,
|
||||
},
|
||||
);
|
||||
|
||||
return classified;
|
||||
} else {
|
||||
throw new Error(`unknown format: ${key.format}`);
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { GENERATOR_DISK } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||
|
||||
import { SecretClassifier } from "./secret-classifier";
|
||||
import { SecretKeyDefinition } from "./secret-key-definition";
|
||||
import { SecretState } from "./secret-state";
|
||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||
|
||||
type FooBar = { foo: boolean; bar: boolean; date?: Date };
|
||||
const classifier = SecretClassifier.allSecret<FooBar>();
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
CombinedState,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||
|
||||
import { ClassifiedFormat } from "./classified-format";
|
||||
import { SecretKeyDefinition } from "./secret-key-definition";
|
||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||
|
||||
const ONE_MINUTE = 1000 * 60;
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
|
||||
/** An encryption strategy that protects a type's secrets with
|
||||
* user-specific keys. This strategy is bound to a specific user.
|
||||
*/
|
||||
export abstract class UserEncryptor {
|
||||
/** Identifies the user bound to the encryptor. */
|
||||
readonly userId: UserId;
|
||||
|
||||
/** Protects secrets in `value` with a user-specific key.
|
||||
* @param secret the object to protect. This object is mutated during encryption.
|
||||
* @returns a promise that resolves to a tuple. The tuple's first property contains
|
||||
* the encrypted secret and whose second property contains an object w/ disclosed
|
||||
* properties.
|
||||
* @throws If `value` is `null` or `undefined`, the promise rejects with an error.
|
||||
*/
|
||||
abstract encrypt<Secret>(secret: Jsonify<Secret>): Promise<EncString>;
|
||||
|
||||
/** Combines protected secrets and disclosed data into a type that can be
|
||||
* rehydrated into a domain object.
|
||||
* @param secret an encrypted JSON payload containing encrypted secrets.
|
||||
* @returns a promise that resolves to the raw state. This state *is not* a
|
||||
* class. It contains only data that can be round-tripped through JSON,
|
||||
* and lacks members such as a prototype or bound functions.
|
||||
* @throws If `secret` or `disclosed` is `null` or `undefined`, the promise
|
||||
* rejects with an error.
|
||||
*/
|
||||
abstract decrypt<Secret>(secret: EncString): Promise<Jsonify<Secret>>;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UserKey } from "../../types/key";
|
||||
|
||||
import { DataPacker } from "./data-packer.abstraction";
|
||||
import { UserKeyEncryptor } from "./user-key-encryptor";
|
||||
|
||||
describe("UserKeyEncryptor", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
const dataPacker = mock<DataPacker>();
|
||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey;
|
||||
const anyUserId = "foo" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
// The UserKeyEncryptor is, in large part, a facade coordinating a handful of worker
|
||||
// objects, so its tests focus on how data flows between components. The defaults rely
|
||||
// on this property--that the facade treats its data like a opaque objects--to trace
|
||||
// the data through several function calls. Should the encryptor interact with the
|
||||
// objects themselves, these mocks will break.
|
||||
encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString));
|
||||
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c as unknown as string));
|
||||
dataPacker.pack.mockImplementation((v) => v as string);
|
||||
dataPacker.unpack.mockImplementation(<T>(v: string) => v as T);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should set userId", async () => {
|
||||
const encryptor = new UserKeyEncryptor(anyUserId, encryptService, userKey, dataPacker);
|
||||
expect(encryptor.userId).toEqual(anyUserId);
|
||||
});
|
||||
|
||||
it("should throw if userId was not supplied", async () => {
|
||||
expect(() => new UserKeyEncryptor(null, encryptService, userKey, dataPacker)).toThrow(
|
||||
"userId cannot be null or undefined",
|
||||
);
|
||||
expect(() => new UserKeyEncryptor(null, encryptService, userKey, dataPacker)).toThrow(
|
||||
"userId cannot be null or undefined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if encryptService was not supplied", async () => {
|
||||
expect(() => new UserKeyEncryptor(anyUserId, null, userKey, dataPacker)).toThrow(
|
||||
"encryptService cannot be null or undefined",
|
||||
);
|
||||
expect(() => new UserKeyEncryptor(anyUserId, null, userKey, dataPacker)).toThrow(
|
||||
"encryptService cannot be null or undefined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if key was not supplied", async () => {
|
||||
expect(() => new UserKeyEncryptor(anyUserId, encryptService, null, dataPacker)).toThrow(
|
||||
"key cannot be null or undefined",
|
||||
);
|
||||
expect(() => new UserKeyEncryptor(anyUserId, encryptService, null, dataPacker)).toThrow(
|
||||
"key cannot be null or undefined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if dataPacker was not supplied", async () => {
|
||||
expect(() => new UserKeyEncryptor(anyUserId, encryptService, userKey, null)).toThrow(
|
||||
"dataPacker cannot be null or undefined",
|
||||
);
|
||||
expect(() => new UserKeyEncryptor(anyUserId, encryptService, userKey, null)).toThrow(
|
||||
"dataPacker cannot be null or undefined",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encrypt", () => {
|
||||
it("should throw if value was not supplied", async () => {
|
||||
const encryptor = new UserKeyEncryptor(anyUserId, encryptService, userKey, dataPacker);
|
||||
|
||||
await expect(encryptor.encrypt<Record<string, never>>(null)).rejects.toThrow(
|
||||
"secret cannot be null or undefined",
|
||||
);
|
||||
await expect(encryptor.encrypt<Record<string, never>>(undefined)).rejects.toThrow(
|
||||
"secret cannot be null or undefined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should encrypt a packed value using the user's key", async () => {
|
||||
const encryptor = new UserKeyEncryptor(anyUserId, encryptService, userKey, dataPacker);
|
||||
const value = { foo: true };
|
||||
|
||||
const result = await encryptor.encrypt(value);
|
||||
|
||||
// these are data flow expectations; the operations all all pass-through mocks
|
||||
expect(dataPacker.pack).toHaveBeenCalledWith(value);
|
||||
expect(encryptService.encrypt).toHaveBeenCalledWith(value, userKey);
|
||||
expect(result).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
it("should throw if secret was not supplied", async () => {
|
||||
const encryptor = new UserKeyEncryptor(anyUserId, encryptService, userKey, dataPacker);
|
||||
|
||||
await expect(encryptor.decrypt(null)).rejects.toThrow("secret cannot be null or undefined");
|
||||
await expect(encryptor.decrypt(undefined)).rejects.toThrow(
|
||||
"secret cannot be null or undefined",
|
||||
);
|
||||
});
|
||||
|
||||
it("should declassify a decrypted packed value using the user's key", async () => {
|
||||
const encryptor = new UserKeyEncryptor(anyUserId, encryptService, userKey, dataPacker);
|
||||
const secret = "encrypted" as any;
|
||||
|
||||
const result = await encryptor.decrypt(secret);
|
||||
|
||||
// these are data flow expectations; the operations all all pass-through mocks
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(secret, userKey);
|
||||
expect(dataPacker.unpack).toHaveBeenCalledWith(secret);
|
||||
expect(result).toBe(secret);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { UserKey } from "../../types/key";
|
||||
|
||||
import { DataPacker } from "./data-packer.abstraction";
|
||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||
|
||||
/** A classification strategy that protects a type's secrets by encrypting them
|
||||
* with a `UserKey`
|
||||
*/
|
||||
export class UserKeyEncryptor extends UserEncryptor {
|
||||
/** Instantiates the encryptor
|
||||
* @param userId identifies the user bound to the encryptor.
|
||||
* @param encryptService protects properties of `Secret`.
|
||||
* @param keyService looks up the user key when protecting data.
|
||||
* @param dataPacker packs and unpacks data classified as secrets.
|
||||
*/
|
||||
constructor(
|
||||
readonly userId: UserId,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly key: UserKey,
|
||||
private readonly dataPacker: DataPacker,
|
||||
) {
|
||||
super();
|
||||
this.assertHasValue("userId", userId);
|
||||
this.assertHasValue("key", key);
|
||||
this.assertHasValue("dataPacker", dataPacker);
|
||||
this.assertHasValue("encryptService", encryptService);
|
||||
}
|
||||
|
||||
async encrypt<Secret>(secret: Jsonify<Secret>): Promise<EncString> {
|
||||
this.assertHasValue("secret", secret);
|
||||
|
||||
let packed = this.dataPacker.pack(secret);
|
||||
const encrypted = await this.encryptService.encrypt(packed, this.key);
|
||||
packed = null;
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
async decrypt<Secret>(secret: EncString): Promise<Jsonify<Secret>> {
|
||||
this.assertHasValue("secret", secret);
|
||||
|
||||
let decrypted = await this.encryptService.decryptToUtf8(secret, this.key);
|
||||
const unpacked = this.dataPacker.unpack<Secret>(decrypted);
|
||||
decrypted = null;
|
||||
|
||||
return unpacked;
|
||||
}
|
||||
|
||||
private assertHasValue(name: string, value: any) {
|
||||
if (value === undefined || value === null) {
|
||||
throw new Error(`${name} cannot be null or undefined`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/st
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec";
|
||||
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||
import { UserBound } from "../dependencies";
|
||||
import { PrivateClassifier } from "../private-classifier";
|
||||
import { StateConstraints } from "../types";
|
||||
|
||||
import { ClassifiedFormat } from "./classified-format";
|
||||
import { ObjectKey } from "./object-key";
|
||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||
import { UserStateSubject } from "./user-state-subject";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
@@ -734,6 +734,7 @@ describe("UserStateSubject", () => {
|
||||
error = e as any;
|
||||
},
|
||||
});
|
||||
singleUserEncryptor$.next({ userId: SomeUser, encryptor: SomeEncryptor });
|
||||
singleUserEncryptor$.next({ userId: errorUserId, encryptor: SomeEncryptor });
|
||||
await awaitAsync();
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
filter,
|
||||
map,
|
||||
takeUntil,
|
||||
pairwise,
|
||||
distinctUntilChanged,
|
||||
BehaviorSubject,
|
||||
startWith,
|
||||
Observable,
|
||||
Subscription,
|
||||
last,
|
||||
@@ -30,15 +28,15 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||
import { UserBound } from "../dependencies";
|
||||
import { anyComplete, ready, withLatestReady } from "../rx";
|
||||
import { anyComplete, errorOnChange, ready, withLatestReady } from "../rx";
|
||||
import { Constraints, SubjectConstraints, WithConstraints } from "../types";
|
||||
|
||||
import { ClassifiedFormat, isClassifiedFormat } from "./classified-format";
|
||||
import { unconstrained$ } from "./identity-state-constraint";
|
||||
import { isObjectKey, ObjectKey, toUserKeyDefinition } from "./object-key";
|
||||
import { isDynamic } from "./state-constraints-dependency";
|
||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||
import { UserStateSubjectDependencies } from "./user-state-subject-dependencies";
|
||||
|
||||
type Constrained<State> = { constraints: Readonly<Constraints<State>>; state: State };
|
||||
@@ -195,24 +193,13 @@ export class UserStateSubject<
|
||||
}
|
||||
}),
|
||||
// fail the stream if the state desyncs from the bound userId
|
||||
startWith({ userId: this.state.userId, encryptor: null } as UserBound<
|
||||
"encryptor",
|
||||
UserEncryptor
|
||||
>),
|
||||
pairwise(),
|
||||
map(([expected, actual]) => {
|
||||
if (expected.userId === actual.userId) {
|
||||
return actual;
|
||||
} else {
|
||||
throw {
|
||||
expectedUserId: expected.userId,
|
||||
actualUserId: actual.userId,
|
||||
};
|
||||
}
|
||||
}),
|
||||
errorOnChange(
|
||||
({ userId }) => userId,
|
||||
(expectedUserId, actualUserId) => ({ expectedUserId, actualUserId }),
|
||||
),
|
||||
// reduce emissions to when encryptor changes
|
||||
distinctUntilChanged(),
|
||||
map(({ encryptor }) => encryptor),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// fail fast if the format is incorrect
|
||||
if (!isClassifiedFormat(input)) {
|
||||
throw new Error(`Cannot declassify ${this.key.key}; unknown format.`);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}),
|
||||
);
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`unknown serialization format: ${this.objectKey.format}`);
|
||||
// secret state's format wraps objects in an array
|
||||
if (this.objectKey && this.objectKey.format === "secret-state") {
|
||||
return map((input) => {
|
||||
if (!Array.isArray(input)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [unwrapped] = input;
|
||||
if (!isClassifiedFormat(unwrapped)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return unwrapped;
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`unsupported serialization format: ${this.objectKey.format}`);
|
||||
}
|
||||
|
||||
private classify(encryptor$: Observable<UserEncryptor>): OperatorFunction<State, unknown> {
|
||||
@@ -359,41 +373,49 @@ export class UserStateSubject<
|
||||
);
|
||||
}
|
||||
|
||||
// if the key supports encryption, enable encryptor support
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
// 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 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 pipe(
|
||||
withLatestReady(encryptor$),
|
||||
concatMap(async ([input, encryptor]) => {
|
||||
// fail fast if there's no value
|
||||
if (input === null || input === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
// 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;
|
||||
}),
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
throw new Error(`unknown serialization format: ${this.objectKey.format}`);
|
||||
throw new Error(`unsupported serialization format: ${this.objectKey.format}`);
|
||||
}
|
||||
|
||||
/** The userId to which the subject is bound.
|
||||
|
||||
Reference in New Issue
Block a user