Rewrite debug log scrubbing logic in Swift

Also introduces additional defense-in-depth improvements.
This commit is contained in:
Evan Hahn 2023-02-28 15:55:00 -06:00 committed by GitHub
parent a24309fe29
commit 41ff3c142b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 186 additions and 191 deletions

View File

@ -268,8 +268,6 @@
34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; };
34480B551FD0A7A400BC14EF /* DebugLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 34480B4D1FD0A7A300BC14EF /* DebugLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B4E1FD0A7A300BC14EF /* DebugLogger.m */; };
34480B571FD0A7A400BC14EF /* OWSScrubbingLogFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 34480B4F1FD0A7A300BC14EF /* OWSScrubbingLogFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; };
34480B591FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B511FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m */; };
34480B5B1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 34480B5A1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch */; };
344A761124B366F4009D69A5 /* FlagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344A761024B366F4009D69A5 /* FlagsViewController.swift */; };
344A761324B36C8C009D69A5 /* TestingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344A761224B36C8C009D69A5 /* TestingViewController.swift */; };
@ -1394,6 +1392,7 @@
F9613CDE2981F15700894B55 /* SqliteUtilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9613CDD2981F15700894B55 /* SqliteUtilTest.swift */; };
F962B38A293F9F1F00765BD8 /* CRC32.swift in Sources */ = {isa = PBXBuildFile; fileRef = F962B389293F9F1F00765BD8 /* CRC32.swift */; };
F962B38C293F9F9F00765BD8 /* CRC32Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = F962B38B293F9F9F00765BD8 /* CRC32Test.swift */; };
F962FF4929AD0C7C00AFA397 /* OWSScrubbingLogFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F962FF4829AD0C7C00AFA397 /* OWSScrubbingLogFormatter.swift */; };
F963164B291AE06C00218FB7 /* OWSScrubbingLogFormatterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F963164A291AE06C00218FB7 /* OWSScrubbingLogFormatterTest.swift */; };
F963F816292D1B5B007DBBBD /* UIButton+SignalUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F963F815292D1B5B007DBBBD /* UIButton+SignalUI.swift */; };
F963F818292D7E53007DBBBD /* FormattedNumberField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F963F817292D7E53007DBBBD /* FormattedNumberField.swift */; };
@ -2525,8 +2524,6 @@
34480B381FD092E300BC14EF /* SignalShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Prefix.pch"; sourceTree = "<group>"; };
34480B4D1FD0A7A300BC14EF /* DebugLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugLogger.h; sourceTree = "<group>"; };
34480B4E1FD0A7A300BC14EF /* DebugLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugLogger.m; sourceTree = "<group>"; };
34480B4F1FD0A7A300BC14EF /* OWSScrubbingLogFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScrubbingLogFormatter.h; sourceTree = "<group>"; };
34480B511FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScrubbingLogFormatter.m; sourceTree = "<group>"; };
34480B5A1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalMessaging-Prefix.pch"; sourceTree = "<group>"; };
344A761024B366F4009D69A5 /* FlagsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagsViewController.swift; sourceTree = "<group>"; };
344A761224B36C8C009D69A5 /* TestingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestingViewController.swift; sourceTree = "<group>"; };
@ -3910,6 +3907,7 @@
F9613CDD2981F15700894B55 /* SqliteUtilTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SqliteUtilTest.swift; sourceTree = "<group>"; };
F962B389293F9F1F00765BD8 /* CRC32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRC32.swift; sourceTree = "<group>"; };
F962B38B293F9F9F00765BD8 /* CRC32Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRC32Test.swift; sourceTree = "<group>"; };
F962FF4829AD0C7C00AFA397 /* OWSScrubbingLogFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSScrubbingLogFormatter.swift; sourceTree = "<group>"; };
F963164A291AE06C00218FB7 /* OWSScrubbingLogFormatterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSScrubbingLogFormatterTest.swift; sourceTree = "<group>"; };
F963F815292D1B5B007DBBBD /* UIButton+SignalUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+SignalUI.swift"; sourceTree = "<group>"; };
F963F817292D7E53007DBBBD /* FormattedNumberField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedNumberField.swift; sourceTree = "<group>"; };
@ -5138,8 +5136,7 @@
346129371FD1B47200532771 /* OWSPreferences.h */,
346129381FD1B47200532771 /* OWSPreferences.m */,
34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */,
34480B4F1FD0A7A300BC14EF /* OWSScrubbingLogFormatter.h */,
34480B511FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m */,
F962FF4829AD0C7C00AFA397 /* OWSScrubbingLogFormatter.swift */,
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */,
3406D31D25DBF70400885B14 /* RefreshEvent.swift */,
45360B8C1F9521F800FA666C /* Searcher.swift */,
@ -9143,7 +9140,6 @@
3464450E22B7F93600A957B1 /* OWSOrphanDataCleaner.h in Headers */,
346129391FD1B47300532771 /* OWSPreferences.h in Headers */,
346129B41FD1F7E800532771 /* OWSProfileManager.h in Headers */,
34480B571FD0A7A400BC14EF /* OWSScrubbingLogFormatter.h in Headers */,
34074F62203D0CBE004596AE /* OWSSounds.h in Headers */,
34612A061FD7238600532771 /* OWSSyncManager.h in Headers */,
34480B5B1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch in Headers */,
@ -10683,7 +10679,7 @@
346129B51FD1F7E800532771 /* OWSProfileManager.m in Sources */,
3470249E2385B6360078D72C /* OWSProfileManager.swift in Sources */,
34641E182088D7E900E2EDE5 /* OWSScreenLock.swift in Sources */,
34480B591FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m in Sources */,
F962FF4929AD0C7C00AFA397 /* OWSScrubbingLogFormatter.swift in Sources */,
34074F61203D0CBE004596AE /* OWSSounds.m in Sources */,
34612A071FD7238600532771 /* OWSSyncManager.m in Sources */,
885C35502370DFD50004BA35 /* OWSSyncManager.swift in Sources */,

View File

@ -5,7 +5,9 @@
import XCTest
import CocoaLumberjack
import Signal
import SignalCoreKit
import SignalServiceKit
@testable import SignalMessaging
final class OWSScrubbingLogFormatterTest: XCTestCase {
private var formatter: OWSScrubbingLogFormatter { OWSScrubbingLogFormatter() }
@ -38,6 +40,18 @@ final class OWSScrubbingLogFormatterTest: XCTestCase {
rawMessage.substring(from: datePrefixLength)
}
func testAttachmentPathScrubbed() {
let testCases: [String] = [
"/Attachments/",
"/foo/bar/Attachments/abc123.txt",
"Something /foo/bar/Attachments/abc123.txt Something"
]
for testCase in testCases {
XCTAssertEqual(format(testCase), "[ REDACTED_CONTAINS_USER_PATH ]")
}
}
func testDataScrubbed_preformatted() {
let testCases: [String: String] = [
"<01>": "[ REDACTED_DATA:01... ]",
@ -156,6 +170,51 @@ final class OWSScrubbingLogFormatterTest: XCTestCase {
}
}
func testGroupIdScrubbed() {
for _ in 1...100 {
let groupIdCount = Bool.random() ? kGroupIdLengthV1 : kGroupIdLengthV2
let groupId = Randomness.generateRandomBytes(groupIdCount)
let groupIdString = TSGroupThread.defaultThreadId(forGroupId: groupId)
let expectedOutput = "Hello [ REDACTED_GROUP_ID:...\(groupIdString.suffix(2)) ]!"
let result = format("Hello \(groupIdString)!")
XCTAssertTrue(
result.contains(expectedOutput),
"Failed to redact group ID: \(groupIdString). Result was \(result)"
)
}
}
func testThingsThatLookLikeGroupIdNotScrubbed() {
let forbiddenBase64Lengths = Set([
kGroupIdLengthV1.base64Length,
kGroupIdLengthV2.base64Length
])
for _ in 1...100 {
let fakeGroupIdCount: Int32 = {
while true {
let result = Int32.random(in: 1...(kGroupIdLengthV2 * 2))
if !forbiddenBase64Lengths.contains(result.base64Length) {
return result
}
}
}()
let fakeGroupId = Randomness.generateRandomBytes(fakeGroupIdCount)
let fakeGroupIdString = TSGroupThread.defaultThreadId(forGroupId: fakeGroupId)
let input = "Hello \(fakeGroupIdString)!"
let result = format(input)
XCTAssertEqual(
stripDate(fromRawMessage: result),
input,
"Should not be affected"
)
}
}
func testNotScrubbed() {
let input = "Some unfiltered string"
let result = format(input)
@ -265,3 +324,7 @@ final class OWSScrubbingLogFormatterTest: XCTestCase {
}
}
}
private extension Int32 {
var base64Length: Int32 { Int32(4 * ceil(Double(self) / 3)) }
}

View File

@ -21,7 +21,6 @@ FOUNDATION_EXPORT const unsigned char SignalMessagingVersionString[];
#import <SignalMessaging/OWSOrphanDataCleaner.h>
#import <SignalMessaging/OWSPreferences.h>
#import <SignalMessaging/OWSProfileManager.h>
#import <SignalMessaging/OWSScrubbingLogFormatter.h>
#import <SignalMessaging/OWSSounds.h>
#import <SignalMessaging/OWSSyncManager.h>
#import <SignalMessaging/ThreadUtil.h>

View File

@ -5,10 +5,10 @@
#import "DebugLogger.h"
#import "OWSPreferences.h"
#import "OWSScrubbingLogFormatter.h"
#import <AudioToolbox/AudioServices.h>
#import <CocoaLumberjack/DDTTYLogger.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalServiceKit/AppContext.h>
#import <SignalServiceKit/AppVersion.h>
#import <SignalServiceKit/OWSFileSystem.h>

View File

@ -1,12 +0,0 @@
//
// Copyright 2014 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSScrubbingLogFormatter : DDLogFileFormatterDefault
@end
NS_ASSUME_NONNULL_END

View File

@ -1,167 +0,0 @@
//
// Copyright 2016 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "OWSScrubbingLogFormatter.h"
NS_ASSUME_NONNULL_BEGIN
@implementation OWSScrubbingLogFormatter
- (NSRegularExpression *)phoneRegex
{
static NSRegularExpression *regex = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error;
regex = [NSRegularExpression regularExpressionWithPattern:@"\\+\\d{7,12}(\\d{3})"
options:0
error:&error];
if (error || !regex) {
OWSFail(@"could not compile regular expression: %@", error);
}
});
return regex;
}
- (NSRegularExpression *)uuidRegex
{
static NSRegularExpression *regex = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Example: AF112388-9F3D-4EBA-A321-CCE01BA2C85D
NSError *error;
regex =
[NSRegularExpression regularExpressionWithPattern:
@"[\\da-f]{8}\\-[\\da-f]{4}\\-[\\da-f]{4}\\-[\\da-f]{4}\\-[\\da-f]{9}([\\da-f]{3})"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error || !regex) {
OWSFail(@"could not compile regular expression: %@", error);
}
});
return regex;
}
- (NSRegularExpression *)dataRegex
{
static NSRegularExpression *regex = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error;
regex = [NSRegularExpression regularExpressionWithPattern:@"<([\\da-f]{2})[\\da-f]{0,6}( [\\da-f]{2,8})*>"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error || !regex) {
OWSFail(@"could not compile regular expression: %@", error);
}
});
return regex;
}
- (NSRegularExpression *)ios13DataRegex
{
static NSRegularExpression *regex = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error;
regex = [NSRegularExpression
regularExpressionWithPattern:@"\\{length = \\d+, bytes = 0x([\\da-f]{2})[\\.\\da-f ]*\\}"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error || !regex) {
OWSFail(@"could not compile regular expression: %@", error);
}
});
return regex;
}
- (NSRegularExpression *)ipV4AddressRegex
{
static NSRegularExpression *regex = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// NOTE: The group matches the last quad of the IPv4 address.
NSError *error;
regex = [NSRegularExpression regularExpressionWithPattern:@"\\d+\\.\\d+\\.\\d+\\.(\\d+)"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error || !regex) {
OWSFail(@"could not compile regular expression: %@", error);
}
});
return regex;
}
- (NSRegularExpression *)longHexRegex
{
static NSRegularExpression *regex = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Any hex string of 14 chars (7 bytes) or more.
// Example: A321CCE01BA2C85D
NSError *error;
regex = [NSRegularExpression regularExpressionWithPattern:@"[\\da-f]{11,}([\\da-f]{3})"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error || !regex) {
OWSFail(@"could not compile regular expression: %@", error);
}
});
return regex;
}
- (NSString *__nullable)formatLogMessage:(DDLogMessage *)logMessage
{
NSString *logString = [super formatLogMessage:logMessage];
NSRegularExpression *phoneRegex = self.phoneRegex;
logString = [phoneRegex stringByReplacingMatchesInString:logString
options:0
range:NSMakeRange(0, [logString length])
withTemplate:@"[ REDACTED_PHONE_NUMBER:xxx$1 ]"];
NSRegularExpression *uuidRegex = self.uuidRegex;
logString = [uuidRegex stringByReplacingMatchesInString:logString
options:0
range:NSMakeRange(0, [logString length])
withTemplate:@"[ REDACTED_UUID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx$1 ]"];
// We capture only the first two characters of the hex string for logging.
// example log line: "Called someFunction with nsData: <01234567 89abcdef>"
// scrubbed output: "Called someFunction with nsData: [ REDACTED_DATA:01 ]"
NSRegularExpression *dataRegex = self.dataRegex;
logString = [dataRegex stringByReplacingMatchesInString:logString
options:0
range:NSMakeRange(0, [logString length])
withTemplate:@"[ REDACTED_DATA:$1... ]"];
// On iOS 13, when built with the 13 SDK, NSData's description has changed
// and needs to be scrubbed specifically.
// example log line: "Called someFunction with nsData: {length = 8, bytes = 0x0123456789abcdef}"
// scrubbed output: "Called someFunction with nsData: [ REDACTED_DATA:96 ]"
NSRegularExpression *ios13DataRegex = self.ios13DataRegex;
logString = [ios13DataRegex stringByReplacingMatchesInString:logString
options:0
range:NSMakeRange(0, [logString length])
withTemplate:@"[ REDACTED_DATA:$1... ]"];
NSRegularExpression *ipV4AddressRegex = self.ipV4AddressRegex;
logString = [ipV4AddressRegex stringByReplacingMatchesInString:logString
options:0
range:NSMakeRange(0, [logString length])
withTemplate:@"[ REDACTED_IPV4_ADDRESS:...$1 ]"];
NSRegularExpression *longHexRegex = self.longHexRegex;
logString = [longHexRegex stringByReplacingMatchesInString:logString
options:0
range:NSMakeRange(0, [logString length])
withTemplate:@"[ REDACTED_HEX:...$1 ]"];
return logString;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,116 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import CocoaLumberjack
import SignalServiceKit
@objc
class OWSScrubbingLogFormatter: DDLogFileFormatterDefault {
private struct Replacement {
let regex: NSRegularExpression
let replacementTemplate: String
init(
pattern: String,
options: NSRegularExpression.Options = [],
replacementTemplate: String
) {
do {
self.regex = try .init(pattern: pattern, options: options)
} catch {
owsFail("Could not compile regular expression: \(error)")
}
self.replacementTemplate = replacementTemplate
}
init(groupIdLength: Int32) {
let prefix = TSGroupThread.groupThreadUniqueIdPrefix
let groupIdBase64StringLength = groupIdLength.base64Length
let unredactedSize = 2
let redactedSize = groupIdBase64StringLength - unredactedSize
// This assertion exists to prevent someone from updating the values and forgetting to
// update things here.
owsAssert(
prefix == "g" &&
groupIdBase64StringLength >= (unredactedSize + 1) &&
groupIdBase64StringLength <= 100
)
let base64Pattern = "A-Za-z0-9+/="
let base64Char = "[\(base64Pattern)]"
let notBase64Char = "[^\(base64Pattern)]"
self.init(
pattern: "(^|\(notBase64Char))\(prefix)\(base64Char){\(redactedSize)}(\(base64Char){\(unredactedSize)})($|\(notBase64Char))",
replacementTemplate: "$1[ REDACTED_GROUP_ID:...$2 ]$3"
)
}
}
private let replacements: [Replacement] = [
.init(
pattern: "\\+\\d{7,12}(\\d{3})",
replacementTemplate: "[ REDACTED_PHONE_NUMBER:xxx$1 ]"
),
// It's important to redact GV2 IDs first because they're longer. If the shorter IDs were
// first, we'd be left with a bunch of partially-redacted GV2 IDs.
.init(groupIdLength: kGroupIdLengthV2),
.init(groupIdLength: kGroupIdLengthV1),
.init(
pattern: "[\\da-f]{8}\\-[\\da-f]{4}\\-[\\da-f]{4}\\-[\\da-f]{4}\\-[\\da-f]{9}([\\da-f]{3})",
options: .caseInsensitive,
replacementTemplate: "[ REDACTED_UUID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx$1 ]"
),
.init(
pattern: "<([\\da-f]{2})[\\da-f]{0,6}( [\\da-f]{2,8})*>",
options: .caseInsensitive,
replacementTemplate: "[ REDACTED_DATA:$1... ]"
),
// On iOS 13, when built with the 13 SDK, NSData's description has changed and needs to be
// scrubbed specifically.
// example log line: "Called someFunction with nsData: {length = 8, bytes = 0x0123456789abcdef}"
// scrubbed output: "Called someFunction with nsData: [ REDACTED_DATA:96 ]"
.init(
pattern: "\\{length = \\d+, bytes = 0x([\\da-f]{2})[\\.\\da-f ]*\\}",
options: .caseInsensitive,
replacementTemplate: "[ REDACTED_DATA:$1... ]"
),
.init(
pattern: "\\d+\\.\\d+\\.\\d+\\.(\\d+)",
replacementTemplate: "[ REDACTED_IPV4_ADDRESS:...$1 ]"
),
.init(
pattern: "[\\da-f]{11,}([\\da-f]{3})",
options: .caseInsensitive,
replacementTemplate: "[ REDACTED_HEX:...$1 ]"
)
]
public override func format(message: DDLogMessage) -> String? {
guard var logString = super.format(message: message) else {
return nil
}
if logString.contains("/Attachments/") {
return "[ REDACTED_CONTAINS_USER_PATH ]"
}
for replacement in replacements {
logString = replacement.regex.stringByReplacingMatches(
in: logString,
range: logString.entireRange,
withTemplate: replacement.replacementTemplate
)
}
return logString
}
}
private extension Int32 {
var base64Length: Int { Int(4 * ceil(Double(self) / 3)) }
}

View File

@ -38,7 +38,7 @@ public extension TSGroupThread {
groupMembership.isLocalUserFullMemberAndAdministrator
}
private static let groupThreadUniqueIdPrefix = "g"
public static let groupThreadUniqueIdPrefix = "g"
private static let uniqueIdMappingStore = SDSKeyValueStore(collection: "TSGroupThread.uniqueIdMappingStore")