// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* eslint-env node */ import is from '@sindresorhus/is'; import { join as pathJoin } from 'path'; import { compose } from 'lodash/fp'; import { escapeRegExp } from 'lodash'; export const APP_ROOT_PATH = pathJoin(__dirname, '..', '..'); const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g; const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{9}([0-9A-F]{3})/gi; const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g; const GROUP_V2_ID_PATTERN = /(groupv2\()([^=)]+)(=?=?\))/g; const REDACTION_PLACEHOLDER = '[REDACTED]'; export type RedactFunction = (value: string) => string; export const _redactPath = (filePath: string): RedactFunction => { if (!is.string(filePath)) { throw new TypeError("'filePath' must be a string"); } const filePathPattern = _pathToRegExp(filePath); return (text: string): string => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } if (!is.regExp(filePathPattern)) { return text; } return text.replace(filePathPattern, REDACTION_PLACEHOLDER); }; }; export const _pathToRegExp = (filePath: string): RegExp | undefined => { try { const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\'); const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\'); const urlEncodedPath = encodeURI(filePath); // Safe `String::replaceAll`: // https://github.com/lodash/lodash/issues/1084#issuecomment-86698786 const patternString = [ filePath, pathWithNormalizedSlashes, pathWithEscapedSlashes, urlEncodedPath, ] .map(escapeRegExp) .join('|'); return new RegExp(patternString, 'g'); } catch (error) { return undefined; } }; // Public API export const redactPhoneNumbers = (text: string): string => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } return text.replace(PHONE_NUMBER_PATTERN, `+${REDACTION_PLACEHOLDER}$1`); }; export const redactUuids = (text: string): string => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } return text.replace(UUID_PATTERN, `${REDACTION_PLACEHOLDER}$1`); }; export const redactGroupIds = (text: string): string => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } return text .replace( GROUP_ID_PATTERN, (_, before, id, after) => `${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice( -3 )}${after}` ) .replace( GROUP_V2_ID_PATTERN, (_, before, id, after) => `${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice( -3 )}${after}` ); }; const createRedactSensitivePaths = ( paths: ReadonlyArray ): RedactFunction => { return compose(paths.map(filePath => _redactPath(filePath))); }; const sensitivePaths: Array = []; let redactSensitivePaths: RedactFunction = (text: string) => text; export const addSensitivePath = (filePath: string): void => { sensitivePaths.push(filePath); redactSensitivePaths = createRedactSensitivePaths(sensitivePaths); }; addSensitivePath(APP_ROOT_PATH); export const redactAll: RedactFunction = compose( (text: string) => redactSensitivePaths(text), redactGroupIds, redactPhoneNumbers, redactUuids ); const removeNewlines: RedactFunction = text => text.replace(/\r?\n|\r/g, '');