Skip to content

Commit

Permalink
Merge pull request #53527 from callstack-internal/feat/mask-emails-wi…
Browse files Browse the repository at this point in the history
…th-random-email

refactor: mask emails with random strings instead of ***
  • Loading branch information
Gonals authored Dec 10, 2024
2 parents 052f3f9 + 836e8e8 commit c79478e
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 12 deletions.
73 changes: 65 additions & 8 deletions src/libs/ExportOnyxState/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Session} from '@src/types/onyx';

const MASKING_PATTERN = '***';
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;

const emailMap = new Map<string, string>();

const getRandomLetter = () => String.fromCharCode(97 + Math.floor(Math.random() * 26));

function stringContainsEmail(text: string) {
return emailRegex.test(text);
}

function extractEmail(text: string) {
const match = text.match(emailRegex);
return match ? match[0] : null; // Return the email if found, otherwise null
}

const randomizeEmail = (email: string): string => {
const [localPart, domain] = email.split('@');
const [domainName, tld] = domain.split('.');

const randomizePart = (part: string) => [...part].map((c) => (/[a-zA-Z0-9]/.test(c) ? getRandomLetter() : c)).join('');
const randomLocal = randomizePart(localPart);
const randomDomain = randomizePart(domainName);

return `${randomLocal}@${randomDomain}.${tld}`;
};

function replaceEmailInString(text: string, emailReplacement: string) {
return text.replace(emailRegex, emailReplacement);
}

const maskSessionDetails = (data: Record<string, unknown>): Record<string, unknown> => {
const session = data.session as Session;
Expand All @@ -22,13 +51,30 @@ const maskSessionDetails = (data: Record<string, unknown>): Record<string, unkno
};
};

const maskEmail = (email: string) => {
let maskedEmail = '';
if (!emailMap.has(email)) {
maskedEmail = randomizeEmail(email);
emailMap.set(email, maskedEmail);
} else {
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
maskedEmail = emailMap.get(email) as string;
}
return maskedEmail;
};

const maskFragileData = (data: Record<string, unknown> | unknown[] | null, parentKey?: string): Record<string, unknown> | unknown[] | null => {
if (data === null) {
return data;
}

if (Array.isArray(data)) {
return data.map((item): unknown => (typeof item === 'object' ? maskFragileData(item as Record<string, unknown>, parentKey) : item));
return data.map((item): unknown => {
if (typeof item === 'string' && Str.isValidEmail(item)) {
return maskEmail(item);
}
return typeof item === 'object' ? maskFragileData(item as Record<string, unknown>, parentKey) : item;
});
}

const maskedData: Record<string, unknown> = {};
Expand All @@ -38,16 +84,26 @@ const maskFragileData = (data: Record<string, unknown> | unknown[] | null, paren
return;
}

const value = data[key];
// loginList is an object that contains emails as keys, the keys should be masked as well
let propertyName = '';
if (Str.isValidEmail(key)) {
propertyName = maskEmail(key);
} else {
propertyName = key;
}

const value = data[propertyName];

if (typeof value === 'string' && Str.isValidEmail(value)) {
maskedData[key] = MASKING_PATTERN;
} else if (parentKey && parentKey.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) && (key === 'text' || key === 'html')) {
maskedData[key] = MASKING_PATTERN;
maskedData[propertyName] = maskEmail(value);
} else if (typeof value === 'string' && stringContainsEmail(value)) {
maskedData[propertyName] = replaceEmailInString(value, maskEmail(extractEmail(value) ?? ''));
} else if (parentKey && parentKey.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) && (propertyName === 'text' || propertyName === 'html')) {
maskedData[propertyName] = MASKING_PATTERN;
} else if (typeof value === 'object') {
maskedData[key] = maskFragileData(value as Record<string, unknown>, key.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) ? key : parentKey);
maskedData[propertyName] = maskFragileData(value as Record<string, unknown>, propertyName.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) ? propertyName : parentKey);
} else {
maskedData[key] = value;
maskedData[propertyName] = value;
}
});

Expand All @@ -63,7 +119,8 @@ const maskOnyxState = (data: Record<string, unknown>, isMaskingFragileDataEnable
onyxState = maskFragileData(onyxState) as Record<string, unknown>;
}

emailMap.clear();
return onyxState;
};

export default {maskOnyxState};
export {maskOnyxState, emailRegex};
2 changes: 1 addition & 1 deletion src/libs/ExportOnyxState/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import RNFS from 'react-native-fs';
import {open} from 'react-native-quick-sqlite';
import Share from 'react-native-share';
import CONST from '@src/CONST';
import ExportOnyxState from './common';
import * as ExportOnyxState from './common';

const readFromOnyxDatabase = () =>
new Promise((resolve) => {
Expand Down
2 changes: 1 addition & 1 deletion src/libs/ExportOnyxState/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CONST from '@src/CONST';
import ExportOnyxState from './common';
import * as ExportOnyxState from './common';

const readFromOnyxDatabase = () =>
new Promise<Record<string, unknown>>((resolve) => {
Expand Down
47 changes: 45 additions & 2 deletions tests/unit/ExportOnyxStateTest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ExportOnyxState from '@libs/ExportOnyxState/common';
import * as ExportOnyxState from '@libs/ExportOnyxState/common';
import type * as OnyxTypes from '@src/types/onyx';

type ExampleOnyxState = {
Expand Down Expand Up @@ -40,6 +40,49 @@ describe('maskOnyxState', () => {

expect(result.session.authToken).toBe('***');
expect(result.session.encryptedAuthToken).toBe('***');
expect(result.session.email).toBe('***');
});

it('should mask emails as a string value in property with a random email', () => {
const input = {
session: mockSession,
};

const result = ExportOnyxState.maskOnyxState(input) as ExampleOnyxState;

expect(result.session.email).toMatch(ExportOnyxState.emailRegex);
});

it('should mask array of emails with random emails', () => {
const input = {
session: mockSession,
emails: ['[email protected]', '[email protected]'],
};

const result = ExportOnyxState.maskOnyxState(input, true) as Record<string, string[]>;

expect(result.emails.at(0)).toMatch(ExportOnyxState.emailRegex);
expect(result.emails.at(1)).toMatch(ExportOnyxState.emailRegex);
});

it('should mask emails in keys of objects', () => {
const input = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'[email protected]': 'value',
session: mockSession,
};

const result = ExportOnyxState.maskOnyxState(input, true) as Record<string, string>;

expect(Object.keys(result).at(0)).toMatch(ExportOnyxState.emailRegex);
});

it('should mask emails that are part of a string', () => {
const input = {
session: mockSession,
emailString: '[email protected] is a test string',
};

const result = ExportOnyxState.maskOnyxState(input, true) as Record<string, string>;
expect(result.emailString).not.toContain('[email protected]');
});
});

0 comments on commit c79478e

Please sign in to comment.