mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-26 01:35:22 +02:00
This allows the file to be checked by tsc, which would have caught some of the missing type aliases sooner (now added to Native.ts.in). Strictly speaking the behavior is slightly different: we have returned to exporting many items individually instead of collecting them on a single object. Co-authored-by: Alex Bakon <akonradi@signal.org>
330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
//
|
|
// Copyright 2024 Signal Messenger, LLC.
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
/**
|
|
* Message backup validation routines.
|
|
*
|
|
* @module MessageBackup
|
|
*/
|
|
|
|
import * as Native from './Native.js';
|
|
import { BackupForwardSecrecyToken, BackupKey } from './AccountKeys.js';
|
|
import { Aci } from './Address.js';
|
|
import { InputStream } from './io.js';
|
|
|
|
export type InputStreamFactory = () => InputStream;
|
|
|
|
/**
|
|
* Result of validating a message backup bundle.
|
|
*/
|
|
export class ValidationOutcome {
|
|
/**
|
|
* A developer-facing message about the error encountered during validation,
|
|
* if any.
|
|
*/
|
|
public errorMessage: string | null;
|
|
|
|
/**
|
|
* Information about unknown fields encountered during validation.
|
|
*/
|
|
public unknownFieldMessages: string[];
|
|
|
|
/**
|
|
* `true` if the backup is valid, `false` otherwise.
|
|
*
|
|
* If this is `true`, there might still be messages about unknown fields.
|
|
*/
|
|
public get ok(): boolean {
|
|
return this.errorMessage == null;
|
|
}
|
|
|
|
constructor(outcome: Native.MessageBackupValidationOutcome) {
|
|
const { errorMessage, unknownFieldMessages } = outcome;
|
|
this.errorMessage = errorMessage;
|
|
this.unknownFieldMessages = unknownFieldMessages;
|
|
}
|
|
}
|
|
|
|
export type MessageBackupKeyInput = Readonly<
|
|
| {
|
|
accountEntropy: string;
|
|
aci: Aci;
|
|
forwardSecrecyToken?: BackupForwardSecrecyToken;
|
|
}
|
|
| {
|
|
backupKey: BackupKey | Uint8Array;
|
|
backupId: Uint8Array;
|
|
forwardSecrecyToken?: BackupForwardSecrecyToken;
|
|
}
|
|
>;
|
|
|
|
/**
|
|
* Key used to encrypt and decrypt a message backup bundle.
|
|
*
|
|
* @see {@link BackupKey}
|
|
*/
|
|
export class MessageBackupKey {
|
|
readonly _nativeHandle: Native.MessageBackupKey;
|
|
|
|
/**
|
|
* Create a backup bundle key from an account entropy pool and ACI.
|
|
*
|
|
* ...or from a backup key and ID, used when reading from a local backup, which may have been
|
|
* created with a different ACI.
|
|
*
|
|
* The account entropy pool must be **validated**; passing an arbitrary string here is considered
|
|
* a programmer error. Similarly, passing a backup key or ID of the wrong length is also an error.
|
|
*/
|
|
public constructor(input: MessageBackupKeyInput) {
|
|
if ('accountEntropy' in input) {
|
|
const { accountEntropy, aci, forwardSecrecyToken } = input;
|
|
this._nativeHandle = Native.MessageBackupKey_FromAccountEntropyPool(
|
|
accountEntropy,
|
|
aci.getServiceIdFixedWidthBinary(),
|
|
forwardSecrecyToken?.contents ?? null
|
|
);
|
|
} else {
|
|
const { backupId, forwardSecrecyToken } = input;
|
|
let { backupKey } = input;
|
|
if (backupKey instanceof BackupKey) {
|
|
backupKey = backupKey.contents;
|
|
}
|
|
this._nativeHandle = Native.MessageBackupKey_FromBackupKeyAndBackupId(
|
|
backupKey,
|
|
backupId,
|
|
forwardSecrecyToken?.contents ?? null
|
|
);
|
|
}
|
|
}
|
|
|
|
/** An HMAC key used to sign a backup file. */
|
|
public get hmacKey(): Uint8Array {
|
|
return Native.MessageBackupKey_GetHmacKey(this);
|
|
}
|
|
|
|
/** An AES-256-CBC key used to encrypt a backup file. */
|
|
public get aesKey(): Uint8Array {
|
|
return Native.MessageBackupKey_GetAesKey(this);
|
|
}
|
|
}
|
|
|
|
// This must match the Rust version of the enum.
|
|
export enum Purpose {
|
|
DeviceTransfer = 0,
|
|
RemoteBackup = 1,
|
|
TakeoutExport = 2,
|
|
}
|
|
|
|
/**
|
|
* Validate a backup file
|
|
*
|
|
* @param backupKey The key to use to decrypt the backup contents.
|
|
* @param purpose Whether the backup is intended for device-to-device transfer or remote storage.
|
|
* @param inputFactory A function that returns new input streams that read the backup contents.
|
|
* @param length The exact length of the input stream.
|
|
* @returns The outcome of validation, including any errors and warnings.
|
|
* @throws IoError If an IO error on the input occurs.
|
|
*
|
|
* @see OnlineBackupValidator
|
|
*/
|
|
export async function validate(
|
|
backupKey: MessageBackupKey,
|
|
purpose: Purpose,
|
|
inputFactory: InputStreamFactory,
|
|
length: bigint
|
|
): Promise<ValidationOutcome> {
|
|
let firstStream: InputStream | undefined;
|
|
let secondStream: InputStream | undefined;
|
|
try {
|
|
firstStream = inputFactory();
|
|
secondStream = inputFactory();
|
|
return new ValidationOutcome(
|
|
await Native.MessageBackupValidator_Validate(
|
|
backupKey,
|
|
firstStream,
|
|
secondStream,
|
|
length,
|
|
purpose
|
|
)
|
|
);
|
|
} finally {
|
|
await firstStream?.close();
|
|
await secondStream?.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An alternative to {@link validate()} that validates a backup frame-by-frame.
|
|
*
|
|
* This is much faster than using `validate()` because it bypasses the decryption and decompression
|
|
* steps, but that also means it's validating less. Don't forget to call `finalize()`!
|
|
*
|
|
* Unlike `validate()`, unknown fields are treated as "soft" errors and logged, rather than
|
|
* collected and returned to the app for processing.
|
|
*
|
|
* # Example
|
|
*
|
|
* ```
|
|
* const validator = new OnlineBackupValidator(
|
|
* backupInfoProto.serialize(),
|
|
* Purpose.deviceTransfer)
|
|
* repeat {
|
|
* // ...generate Frames...
|
|
* validator.addFrame(frameProto.serialize())
|
|
* }
|
|
* validator.finalize() // don't forget this!
|
|
* ```
|
|
*/
|
|
export class OnlineBackupValidator {
|
|
readonly _nativeHandle: Native.OnlineBackupValidator;
|
|
|
|
/**
|
|
* Initializes an OnlineBackupValidator from the given BackupInfo protobuf message.
|
|
*
|
|
* "Soft" errors will be logged, including unrecognized fields in the protobuf.
|
|
*
|
|
* @throws BackupValidationError on error
|
|
*/
|
|
constructor(backupInfo: Uint8Array, purpose: Purpose) {
|
|
this._nativeHandle = Native.OnlineBackupValidator_New(backupInfo, purpose);
|
|
}
|
|
|
|
/**
|
|
* Processes a single Frame protobuf message.
|
|
*
|
|
* "Soft" errors will be logged, including unrecognized fields in the protobuf.
|
|
*
|
|
* @throws BackupValidationError on error
|
|
*/
|
|
addFrame(frame: Uint8Array): void {
|
|
Native.OnlineBackupValidator_AddFrame(this, frame);
|
|
}
|
|
|
|
/**
|
|
* Marks that a backup is complete, and does any final checks that require whole-file knowledge.
|
|
*
|
|
* "Soft" errors will be logged.
|
|
*
|
|
* @throws BackupValidationError on error
|
|
*/
|
|
finalize(): void {
|
|
Native.OnlineBackupValidator_Finalize(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An in-memory representation of a backup file used to compare contents.
|
|
*
|
|
* When comparing the contents of two backups:
|
|
* 1. Create a `ComparableBackup` instance for each of the inputs.
|
|
* 2. Check the `unknownFields()` value; if it's not empty, some parts of the
|
|
* backup weren't parsed and won't be compared.
|
|
* 3. Produce a canonical string for each backup with `comparableString()`.
|
|
* 4. Compare the canonical string representations.
|
|
*
|
|
* The diff of the canonical strings (which may be rather large) will show the
|
|
* differences between the logical content of the input backup files.
|
|
*/
|
|
export class ComparableBackup {
|
|
readonly _nativeHandle: Native.ComparableBackup;
|
|
constructor(handle: Native.ComparableBackup) {
|
|
this._nativeHandle = handle;
|
|
}
|
|
|
|
/**
|
|
* Read an unencrypted backup file into memory for comparison.
|
|
*
|
|
* @param purpose Whether the backup is intended for device-to-device transfer or remote storage.
|
|
* @param input An input stream that reads the backup contents.
|
|
* @param length The exact length of the input stream.
|
|
* @returns The in-memory representation.
|
|
* @throws BackupValidationError If an IO error occurs or the input is invalid.
|
|
*/
|
|
public static async fromUnencrypted(
|
|
purpose: Purpose,
|
|
input: InputStream,
|
|
length: bigint
|
|
): Promise<ComparableBackup> {
|
|
const handle = await Native.ComparableBackup_ReadUnencrypted(
|
|
input,
|
|
length,
|
|
purpose
|
|
);
|
|
return new ComparableBackup(handle);
|
|
}
|
|
|
|
/**
|
|
* Produces a string representation of the contents.
|
|
*
|
|
* The returned strings for two backups will be equal if the backups contain
|
|
* the same logical content. If two backups' strings are not equal, the diff
|
|
* will show what is different between them.
|
|
*
|
|
* @returns a canonical string representation of the backup
|
|
*/
|
|
public comparableString(): string {
|
|
return Native.ComparableBackup_GetComparableString(this);
|
|
}
|
|
|
|
/**
|
|
* Unrecognized protobuf fields present in the backup.
|
|
*
|
|
* If this is not empty, some parts of the backup were not recognized and
|
|
* won't be present in the string representation.
|
|
*/
|
|
public get unknownFields(): Array<string> {
|
|
return Native.ComparableBackup_GetUnknownFields(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Streaming exporter that produces a human-readable JSON representation of a backup.
|
|
*/
|
|
export class BackupJsonExporter {
|
|
readonly _nativeHandle: Native.BackupJsonExporter;
|
|
|
|
private constructor(handle: Native.BackupJsonExporter) {
|
|
this._nativeHandle = handle;
|
|
}
|
|
|
|
/**
|
|
* Initializes the streaming exporter and returns the first chunk of output.
|
|
* @param backupInfo The serialized BackupInfo protobuf without a varint header.
|
|
* @param [options] Additional configuration for the exporter.
|
|
* @param [options.validate=true] Whether to run semantic validation on the backup.
|
|
* @returns An object containing the exporter and the first chunk of output, containing the backup info.
|
|
* @throws Error if the input is invalid.
|
|
*/
|
|
public static start(
|
|
backupInfo: Uint8Array,
|
|
options?: { validate?: boolean }
|
|
): { exporter: BackupJsonExporter; chunk: string } {
|
|
const shouldValidate = options?.validate ?? true;
|
|
const handle = Native.BackupJsonExporter_New(backupInfo, shouldValidate);
|
|
const exporter = new BackupJsonExporter(handle);
|
|
const chunk = Native.BackupJsonExporter_GetInitialChunk(exporter);
|
|
return { exporter, chunk };
|
|
}
|
|
|
|
/**
|
|
* Validates and exports a human-readable JSON representation of backup frames.
|
|
* @param frames One or more varint delimited Frame serialized protobuf messages.
|
|
* @returns A string containing the exported frames.
|
|
* @throws Error if the input is invalid.
|
|
*/
|
|
public exportFrames(frames: Uint8Array): string {
|
|
return Native.BackupJsonExporter_ExportFrames(this, frames);
|
|
}
|
|
|
|
/**
|
|
* Completes the validation and export of the previously exported frames.
|
|
* @returns A string containing the final chunk of the output.
|
|
* @throws Error if some previous input fails validation at the final stage.
|
|
*/
|
|
public finish(): string {
|
|
return Native.BackupJsonExporter_Finish(this);
|
|
}
|
|
}
|