mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
* introduce `GeneratorHistoryService` abstraction * implement generator history service with `LocalGeneratorHistoryService` * cache decrypted data using `ReplaySubject` instead of `DerivedState` * move Jsonification from `DataPacker` to `SecretClassifier` because the classifier is the only component that has full type information. The data packer still handles stringification.
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import { Jsonify } from "type-fest";
|
|
|
|
import { Utils } from "../../../platform/misc/utils";
|
|
|
|
import { DataPacker as DataPackerAbstraction } from "./data-packer.abstraction";
|
|
|
|
const DATA_PACKING = Object.freeze({
|
|
/** The character to use for padding. */
|
|
padding: "0",
|
|
|
|
/** The character dividing packed data. */
|
|
divider: "|",
|
|
|
|
/** A regular expression for detecting invalid padding. When the character
|
|
* changes, this should be updated to include the new padding pattern.
|
|
*/
|
|
hasInvalidPadding: /[^0]/,
|
|
});
|
|
|
|
/** A packing strategy that conceals the length of secret data by padding it
|
|
* to a multiple of the frame size.
|
|
* @example
|
|
* // packed === "24|e2Zvbzp0cnVlfQ==|0000"
|
|
* const packer = new SecretPacker(24);
|
|
* const packed = packer.pack({ foo: true });
|
|
*/
|
|
export class PaddedDataPacker extends DataPackerAbstraction {
|
|
/** Instantiates the padded data packer
|
|
* @param frameSize The size of the dataframe used to pad encrypted values.
|
|
*/
|
|
constructor(private readonly frameSize: number) {
|
|
super();
|
|
}
|
|
|
|
/**
|
|
* Packs value into a string format that conceals the length by obscuring it
|
|
* with the frameSize.
|
|
* @see {@link DataPackerAbstraction.unpack}
|
|
*/
|
|
pack<Secret>(value: Jsonify<Secret>) {
|
|
// encode the value
|
|
const json = JSON.stringify(value);
|
|
const b64 = Utils.fromUtf8ToB64(json);
|
|
|
|
// calculate packing metadata
|
|
const frameSize = JSON.stringify(this.frameSize);
|
|
const separatorLength = 2 * DATA_PACKING.divider.length; // there are 2 separators
|
|
const payloadLength = b64.length + frameSize.length + separatorLength;
|
|
const paddingLength = this.frameSize - (payloadLength % this.frameSize);
|
|
|
|
// pack the data, thereby concealing its length
|
|
const padding = DATA_PACKING.padding.repeat(paddingLength);
|
|
const packed = `${frameSize}|${b64}|${padding}`;
|
|
|
|
return packed;
|
|
}
|
|
|
|
/** {@link DataPackerAbstraction.unpack} */
|
|
unpack<Secret>(secret: string): Jsonify<Secret> {
|
|
// frame size is stored before the JSON payload in base 10
|
|
const frameBreakpoint = secret.indexOf(DATA_PACKING.divider);
|
|
if (frameBreakpoint < 1) {
|
|
throw new Error("missing frame size");
|
|
}
|
|
const frameSize = parseInt(secret.slice(0, frameBreakpoint), 10);
|
|
|
|
// The decrypted string should be a multiple of the frame length
|
|
if (secret.length % frameSize > 0) {
|
|
throw new Error("invalid length");
|
|
}
|
|
|
|
// encoded data terminates with the divider, followed by the padding character
|
|
const jsonBreakpoint = secret.lastIndexOf(DATA_PACKING.divider);
|
|
if (jsonBreakpoint == frameBreakpoint) {
|
|
throw new Error("missing json object");
|
|
}
|
|
const paddingBegins = jsonBreakpoint + 1;
|
|
|
|
// If the padding contains invalid padding characters then the padding could be used
|
|
// as a side channel for arbitrary data.
|
|
if (secret.slice(paddingBegins).match(DATA_PACKING.hasInvalidPadding)) {
|
|
throw new Error("invalid padding");
|
|
}
|
|
|
|
// remove frame size and padding
|
|
const b64 = secret.substring(frameBreakpoint, paddingBegins);
|
|
|
|
// unpack the stored data
|
|
const json = Utils.fromB64ToUtf8(b64);
|
|
const unpacked = JSON.parse(json);
|
|
|
|
return unpacked;
|
|
}
|
|
}
|