1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00
Files
browser/libs/common/src/tools/generator/state/padded-data-packer.ts
✨ Audrey ✨ df058ba399 [PM-6146] generator history (#8497)
* 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.
2024-03-28 12:19:12 -04:00

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;
}
}