Просмотр исходного кода

Allow to opt-in sensitive data when using the report tool (#875)

Lennart Grahl 6 лет назад
Родитель
Сommit
a1af5b2d25

+ 1 - 0
public/i18n/de.json

@@ -72,6 +72,7 @@
         "REPORT_LOG": "Sollte das FAQ Ihre Frage nicht beantworten oder das Problem bestehen bleiben, können Sie einen Fehlerbericht an den Threema Support senden.",
         "REPORT_VIA_THREEMA_UNAVAILABLE": "Das Senden eines Fehlerberichts ist zur Zeit nicht möglich, da keine Verbindung zu Ihrem Mobilgerät besteht. Stellen Sie zuerst eine Verbindung mit Ihrem Mobilgerät her.",
         "REPORT_VIA_CLIPBOARD": "Alternativ können Sie den Fehlerbericht in die Zwischenablage kopieren und <a target=\"_blank\" href=\"https://threema.ch/support\">per Webformular an den Threema Support senden</a>",
+        "REMOVE_SENSITIVE_DATA": "Sensible Daten entfernen (Kontakte, Nachrichteninhalte, etc.)",
         "DESCRIBE_PROBLEM": "Bitte beschreiben Sie das Problem.",
         "COPY_LOG_CLIPBOARD": "In die Zwischenablage kopieren",
         "REPORT_VIA_THREEMA": "Fehlerbericht an *SUPPORT senden",

+ 1 - 0
public/i18n/en.json

@@ -72,6 +72,7 @@
         "REPORT_LOG": "If you can't find an answer to your question or the problem persists, you can send a log report to the Threema support team.",
         "REPORT_VIA_THREEMA_UNAVAILABLE": "Reporting the log via Threema is currently unavailable since there is no connection to your mobile device. Connect to your mobile device first.",
         "REPORT_VIA_CLIPBOARD": "Alternatively, you may copy the log into the clipboard and <a target=\"_blank\" href=\"https://threema.ch/support\">report it to the Threema support team via the web form</a>.",
+        "REMOVE_SENSITIVE_DATA": "Remove sensitive data (contacts, message contents, etc.)",
         "DESCRIBE_PROBLEM": "Please describe the problem here.",
         "COPY_LOG_CLIPBOARD": "Copy log to clipboard",
         "REPORT_VIA_THREEMA": "Send log to *SUPPORT",

+ 16 - 13
src/controllers/troubleshooting.ts

@@ -20,7 +20,6 @@ import {Logger} from 'ts-log';
 import {arrayToBuffer, copyShallow, hasFeature, sleep} from '../helpers';
 import * as clipboard from '../helpers/clipboard';
 
-import {MemoryLogger} from '../helpers/logger';
 import {BrowserService} from '../services/browser';
 import {LogService} from '../services/log';
 import {WebClientService} from '../services/webclient';
@@ -40,6 +39,7 @@ export class TroubleshootingController extends DialogController {
     private readonly browserService: BrowserService;
     private readonly webClientService: WebClientService;
     private readonly log: Logger;
+    public sanitize: boolean = true;
     public isSending: boolean = false;
     public sendingFailed: boolean = false;
     public description: string = '';
@@ -94,7 +94,7 @@ export class TroubleshootingController extends DialogController {
         this.sendingFailed = false;
 
         // Get the log
-        const log = new TextEncoder().encode(this.getLog());
+        const log = new TextEncoder().encode(this.getLog(this.sanitize));
 
         // Error handler
         const fail = () => {
@@ -165,7 +165,7 @@ export class TroubleshootingController extends DialogController {
      */
     public copyToClipboard(): void {
         // Get the log
-        const log = this.getLog();
+        const log = this.getLog(this.sanitize);
 
         // Copy to clipboard
         let toastString = 'messenger.COPIED';
@@ -185,20 +185,22 @@ export class TroubleshootingController extends DialogController {
     /**
      * Serialise the memory log and add some metadata.
      */
-    private getLog(): string {
+    private getLog(sanitize: boolean): string {
         const browser = this.browserService.getBrowser();
 
         // Sanitise usernames and credentials from ICE servers in config
         const config = copyShallow(this.config) as threema.Config;
-        config.ICE_SERVERS = config.ICE_SERVERS.map((server: RTCIceServer) => {
-            server = copyShallow(server) as RTCIceServer;
-            for (const key of ['username', 'credential', 'credentialType']) {
-                if (server[key] !== undefined) {
-                    server[key] = `[${server[key].constructor.name}]`;
+        if (sanitize) {
+            config.ICE_SERVERS = config.ICE_SERVERS.map((server: RTCIceServer) => {
+                server = copyShallow(server) as RTCIceServer;
+                for (const key of ['username', 'credential', 'credentialType']) {
+                    if (server[key] !== undefined) {
+                        server[key] = `[${server[key].constructor.name}]`;
+                    }
                 }
-            }
-            return server;
-        });
+                return server;
+            });
+        }
 
         // Create container for meta data and log records
         const container = {
@@ -208,6 +210,7 @@ export class TroubleshootingController extends DialogController {
         };
 
         // Return serialised and sanitised
-        return JSON.stringify(container, MemoryLogger.replacer, 2);
+        const replacer = this.logService.memory.getReplacer(sanitize);
+        return JSON.stringify(container, replacer, 2);
     }
 }

+ 40 - 37
src/helpers/logger.ts

@@ -230,60 +230,63 @@ export class MemoryLogger implements Logger {
     }
 
     /**
-     * Replacer function for serialising log records to JSON.
+     * Create a replacer function for serialising log records to JSON.
      *
-     * A recursive filter will be applied:
+     * The replacer function can be used as a recursive filter for JSON
+     * serialisation. It will filter values in the following way:
      *
      * - the types `null`, `string`, `number` and `boolean` will be returned
      *   unmodified,
      * - an object implementing the `Confidential` interface will be returned
-     *   sanitised,
+     *   sanitised if requested, otherwise as is,
      * - an `Error` instance will be left as is,
      * - the binary types `Uint8Array` and `Blob` will only return meta
      *   information about the content, and
      * - everything else will return the value's type instead of the value
      *   itself.
      */
-    public static replacer(key: string, value: any): any {
-        // Handle `null` and `undefined` early
-        if (value === null || value === undefined) {
-            return value;
-        }
+    public getReplacer(sanitize: boolean): (key: string, value: any) => any {
+        return (key: string, value: any) => {
+            // Handle `null` and `undefined` early
+            if (value === null || value === undefined) {
+                return value;
+            }
 
-        // Apply filter to confidential data
-        if (value instanceof BaseConfidential) {
-            return value.censored();
-        }
+            // Apply filter to confidential data
+            if (value instanceof BaseConfidential) {
+                value = sanitize ? value.censored() : value.uncensored;
+            }
 
-        // Allowed (standard) types
-        for (const allowedType of ALLOWED_TYPES) {
-            if (value.constructor === allowedType) {
-                return value;
+            // Allowed (standard) types
+            for (const allowedType of ALLOWED_TYPES) {
+                if (value.constructor === allowedType) {
+                    return value;
+                }
             }
-        }
 
-        // Allow exceptions
-        if (value instanceof Error) {
-            return value.toString();
-        }
+            // Allow exceptions
+            if (value instanceof Error) {
+                return value.toString();
+            }
 
-        // Filter binary data
-        if (value instanceof ArrayBuffer) {
-            return `[ArrayBuffer: length=${value.byteLength}]`;
-        }
-        if (value instanceof Uint8Array) {
-            return `[Uint8Array: length=${value.byteLength}, offset=${value.byteOffset}]`;
-        }
-        if (value instanceof Blob) {
-            return `[Blob: length=${value.size}, type=${value.type}]`;
-        }
+            // Filter binary data
+            if (value instanceof ArrayBuffer) {
+                return `[ArrayBuffer: length=${value.byteLength}]`;
+            }
+            if (value instanceof Uint8Array) {
+                return `[Uint8Array: length=${value.byteLength}, offset=${value.byteOffset}]`;
+            }
+            if (value instanceof Blob) {
+                return `[Blob: length=${value.size}, type=${value.type}]`;
+            }
 
-        // Plain object
-        if (value.constructor === Object) {
-            return value;
-        }
+            // Plain object
+            if (value.constructor === Object) {
+                return value;
+            }
 
-        // Not listed
-        return `[${value.constructor.name}]`;
+            // Not listed
+            return `[${value.constructor.name}]`;
+        };
     }
 }

+ 12 - 4
src/partials/dialog.troubleshooting.html

@@ -19,10 +19,18 @@
                 <p ng-if="!ctrl.isConnected" translate>troubleshooting.REPORT_VIA_THREEMA_UNAVAILABLE</p>
                 <p ng-if="!ctrl.isConnected || ctrl.sendingFailed" translate>troubleshooting.REPORT_VIA_CLIPBOARD</p>
 
-                <md-input-container ng-if="ctrl.isConnected" class="md-block">
-                    <label translate>troubleshooting.DESCRIBE_PROBLEM</label>
-                    <textarea ng-model="ctrl.description" rows="2" max-rows="10" md-select-on-focus></textarea>
-                </md-input-container>
+                <p>
+                    <md-input-container ng-if="ctrl.isConnected" class="md-block md-hide-errors-spacer">
+                        <label translate>troubleshooting.DESCRIBE_PROBLEM</label>
+                        <textarea ng-model="ctrl.description" rows="2" max-rows="10" md-select-on-focus></textarea>
+                    </md-input-container>
+                </p>
+
+                <p>
+                    <md-checkbox ng-model="ctrl.sanitize" aria-label="Remove sensitive data">
+                        <span translate>troubleshooting.REMOVE_SENSITIVE_DATA</span>
+                    </md-checkbox>
+                </p>
             </div>
         </md-dialog-content>
 

+ 1 - 0
src/sass/app.scss

@@ -25,6 +25,7 @@
 // Components: Micro layout files. Your styles for buttons and navigation and
 // similar page components.
 //@import 'components/lists';
+@import 'components/input';
 @import 'components/spinner';
 @import 'components/links';
 @import 'components/scrollbars';

+ 6 - 0
src/sass/components/_input.scss

@@ -0,0 +1,6 @@
+// Do not display an error spacer underneath input/textarea fields
+.md-hide-errors-spacer {
+    .md-errors-spacer {
+        display: none;
+    }
+}

+ 0 - 4
src/sass/sections/_navigation.scss

@@ -76,10 +76,6 @@
                 padding: $main-padding;
                 width: 100%;
                 box-sizing: border-box;
-
-                .md-errors-spacer {
-                    display: none;
-                }
             }
         }
 

+ 0 - 2
tests/bootstrap.ts

@@ -25,8 +25,6 @@ import('../src/app')
     .then(() => {
         // @ts-ignore
         window.config = config;
-        // @ts-ignore
-        window.MemoryLogger = MemoryLogger;
         console.info('Bundle loaded')
     })
     .catch((e) => console.error('Could not load bundle', e));

+ 4 - 4
tests/service/log.js

@@ -94,7 +94,7 @@ describe('LogService', function() {
             // Expect the memory logger to have been called for 'debug' and above
             // (i.e. all log levels).
             expect(JSON
-                .parse(JSON.stringify($service.memory.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify($service.memory.getRecords(), $service.memory.getReplacer(true)))
                 .map((record) => record.slice(1))
             ).toEqual([
                 ['debug', '[test]', 'debug'],
@@ -116,7 +116,7 @@ describe('LogService', function() {
 
             // Expect the memory logger tag to be unpadded
             expect(JSON
-                .parse(JSON.stringify($service.memory.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify($service.memory.getRecords(), $service.memory.getReplacer(true)))
                 .map((record) => record.slice(1))
             ).toEqual([
                 ['info', '[test]', 'test']
@@ -135,7 +135,7 @@ describe('LogService', function() {
 
             // Expect the memory logger tag to be unpadded and unstyled
             expect(JSON
-                .parse(JSON.stringify($service.memory.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify($service.memory.getRecords(), $service.memory.getReplacer(true)))
                 .map((record) => record.slice(1))
             ).toEqual([
                 ['info', '%c[test]', style, 'test']
@@ -155,7 +155,7 @@ describe('LogService', function() {
 
             // Expect the memory logger to only contain the 'info' log
             expect(JSON
-                .parse(JSON.stringify($service.memory.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify($service.memory.getRecords(), $service.memory.getReplacer(true)))
                 .map((record) => record.slice(1))
             ).toEqual([
                 ['info', '[test]', 'info']

+ 26 - 13
tests/ts/logger_helpers.ts

@@ -298,7 +298,7 @@ describe('Logger Helpers', () => {
             }
             const end = Date.now();
             const timestamps = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry[0]);
             expect(timestamps.length).toBe(10);
             for (const timestamp of timestamps) {
@@ -323,7 +323,7 @@ describe('Logger Helpers', () => {
             ];
             logger.debug(...record.slice(1));
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(record);
@@ -344,7 +344,7 @@ describe('Logger Helpers', () => {
             ];
             logger.debug('  te%cst  ', 'color: #fff', ...args);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual((['debug', 'te%cst', 'color: #fff'] as any[]).concat(args));
@@ -371,7 +371,7 @@ describe('Logger Helpers', () => {
             ];
             logger.debug(...record.slice(1));
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(record);
@@ -391,25 +391,38 @@ describe('Logger Helpers', () => {
             ];
             logger.debug(...record.slice(1));
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(record);
         });
 
-        it('serialises confidential types sanitised', () => {
+        it('serialises confidential types sanitised, if requested', () => {
             const logger = new MemoryLogger();
 
             // Ensure 'Confidential' messages are being sanitised.
             const confidential = new TestConfidential();
             logger.debug(confidential, confidential, confidential);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(['debug', 'censored', 'censored', 'censored']);
         });
 
+        it('serialises confidential types as is, if requested', () => {
+            const logger = new MemoryLogger();
+
+            // Ensure 'Confidential' messages are being sanitised.
+            const confidential = new TestConfidential();
+            logger.debug(confidential, confidential, confidential);
+            const records = JSON
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(false)))
+                .map((entry) => entry.slice(1));
+            expect(records.length).toBe(1);
+            expect(records[0]).toEqual(['debug', 'uncensored', 'uncensored', 'uncensored']);
+        });
+
         it('serialises exceptions', () => {
             const logger = new MemoryLogger();
 
@@ -417,7 +430,7 @@ describe('Logger Helpers', () => {
             const error = new Error('WTF!');
             logger.error(error);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(['error', error.toString()]);
@@ -433,7 +446,7 @@ describe('Logger Helpers', () => {
             const blob = new Blob([JSON.stringify({ a: 10 })], { type: 'application/json'} );
             logger.debug(buffer, array, blob);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual([
@@ -450,7 +463,7 @@ describe('Logger Helpers', () => {
             // Ensure instances are being represented with their name.
             logger.debug(logger);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(['debug', '[MemoryLogger]']);
@@ -469,7 +482,7 @@ describe('Logger Helpers', () => {
             };
             logger.debug(object);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(['debug', object]);
@@ -486,7 +499,7 @@ describe('Logger Helpers', () => {
             ];
             logger.debug(array);
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records.length).toBe(1);
             expect(records[0]).toEqual(['debug', array]);
@@ -500,7 +513,7 @@ describe('Logger Helpers', () => {
                 logger.debug(i);
             }
             const records = JSON
-                .parse(JSON.stringify(logger.getRecords(), MemoryLogger.replacer))
+                .parse(JSON.stringify(logger.getRecords(), logger.getReplacer(true)))
                 .map((entry) => entry.slice(1));
             expect(records).toEqual([
                 ['debug', 8],