Skip to content
41 changes: 36 additions & 5 deletions src/libs/ExportOnyxState/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import {Str} from 'expensify-common';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Session} from '@src/types/onyx';

const MASKING_PATTERN = '***';

const maskSessionDetails = (data: Record<string, unknown>): Record<string, unknown> => {
const session = data.session as Session;
const maskedData: Record<string, unknown> = {};

Object.keys(session).forEach((key) => {
if (key !== 'authToken' && key !== 'encryptedAuthToken') {
maskedData[key] = session[key as keyof Session];
return;
}
maskedData[key] = MASKING_PATTERN;
});

return {
...data,
session: maskedData,
};
};

const maskFragileData = (data: Record<string, unknown>, parentKey?: string): Record<string, unknown> => {
const maskedData: Record<string, unknown> = {};
Expand All @@ -15,10 +36,10 @@ const maskFragileData = (data: Record<string, unknown>, parentKey?: string): Rec

const value = data[key];

if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) {
maskedData[key] = '***';
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] = '***';
maskedData[key] = MASKING_PATTERN;
} else if (typeof value === 'object') {
maskedData[key] = maskFragileData(value as Record<string, unknown>, key.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) ? key : parentKey);
} else {
Expand All @@ -29,6 +50,16 @@ const maskFragileData = (data: Record<string, unknown>, parentKey?: string): Rec
return maskedData;
};

export default {
maskFragileData,
const maskOnyxState = (data: Record<string, unknown>, isMaskingFragileDataEnabled?: boolean) => {
let onyxState = data;
// Mask session details by default
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Mask session details by default
// Mask session details by default

onyxState = maskSessionDetails(onyxState);
// Mask fragile data other than session details if the user has enabled the option
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Mask fragile data other than session details if the user has enabled the option
// Mask fragile data other than session details if the user has enabled the option

if (isMaskingFragileDataEnabled) {
onyxState = maskFragileData(onyxState);
}

return onyxState;
};

export default {maskOnyxState};
13 changes: 8 additions & 5 deletions src/libs/ExportOnyxState/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ 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 common from './common';
import ExportOnyxState from './common';

const readFromOnyxDatabase = () =>
new Promise((resolve) => {
const db = open({name: CONST.DEFAULT_DB_NAME});
const query = `SELECT * FROM ${CONST.DEFAULT_TABLE_NAME}`;

db.executeAsync(query, []).then(({rows}) => {
// eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-unsafe-member-access
const result = rows?._array.map((row) => ({[row?.record_key]: JSON.parse(row?.valueJSON as string) as unknown}));

// eslint-disable-next-line no-underscore-dangle
const result = rows?._array.reduce<Record<string, unknown>>((acc, row) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
acc[row?.record_key] = JSON.parse(row?.valueJSON as string) as unknown;
return acc;
}, {});
resolve(result);
});
});
Expand All @@ -36,7 +39,7 @@ const shareAsFile = (value: string) => {
};

export default {
maskFragileData: common.maskFragileData,
maskOnyxState: ExportOnyxState.maskOnyxState,
readFromOnyxDatabase,
shareAsFile,
};
4 changes: 2 additions & 2 deletions 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 common from './common';
import ExportOnyxState from './common';

const readFromOnyxDatabase = () =>
new Promise<Record<string, unknown>>((resolve) => {
Expand Down Expand Up @@ -44,7 +44,7 @@ const shareAsFile = (value: string) => {
};

export default {
maskFragileData: common.maskFragileData,
maskOnyxState: ExportOnyxState.maskOnyxState,
readFromOnyxDatabase,
shareAsFile,
};
6 changes: 1 addition & 5 deletions src/pages/settings/Troubleshoot/TroubleshootPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,7 @@ function TroubleshootPage({shouldStoreLogs, shouldMaskOnyxState}: TroubleshootPa

const exportOnyxState = useCallback(() => {
ExportOnyxState.readFromOnyxDatabase().then((value: Record<string, unknown>) => {
let dataToShare = value;
if (shouldMaskOnyxState) {
dataToShare = ExportOnyxState.maskFragileData(value);
}

const dataToShare = ExportOnyxState.maskOnyxState(value, shouldMaskOnyxState);
ExportOnyxState.shareAsFile(JSON.stringify(dataToShare));
});
}, [shouldMaskOnyxState]);
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/ExportOnyxStateTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ExportOnyxState from '@libs/ExportOnyxState/common';
import type * as OnyxTypes from '@src/types/onyx';

type ExampleOnyxState = {
session: OnyxTypes.Session;
};

describe('maskOnyxState', () => {
const mockSession = {
authToken: 'sensitive-auth-token',
encryptedAuthToken: 'sensitive-encrypted-token',
email: 'user@example.com',
accountID: 12345,
};

it('should mask session details by default', () => {
const input = {session: mockSession};
const result = ExportOnyxState.maskOnyxState(input) as ExampleOnyxState;

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

it('should not mask fragile data when isMaskingFragileDataEnabled is false', () => {
const input = {
session: mockSession,
};
const result = ExportOnyxState.maskOnyxState(input) as ExampleOnyxState;

expect(result.session.authToken).toBe('***');
expect(result.session.encryptedAuthToken).toBe('***');
expect(result.session.email).toBe('user@example.com');
});

it('should mask fragile data when isMaskingFragileDataEnabled is true', () => {
const input = {
session: mockSession,
};
const result = ExportOnyxState.maskOnyxState(input, true) as ExampleOnyxState;

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