Skip to content

Commit

Permalink
Merge pull request #1279 from DataDog/ncreated/REPLAY-1610/align-priv…
Browse files Browse the repository at this point in the history
…acy-rules

REPLAY-1610 Align text and appearance masking rules
  • Loading branch information
ncreated authored May 10, 2023
2 parents 5740c36 + 667852a commit 91f9146
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="5e8-XJ-d05">
<rect key="frame" x="20" y="79" width="353" height="617.33333333333337"/>
<rect key="frame" x="20" y="79" width="353" height="686"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nE2-nF-tR0">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Labels" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nE2-nF-tR0">
<rect key="frame" x="0.0" y="0.0" width="353" height="36"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="30"/>
Expand All @@ -101,7 +101,7 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="Text Views" textAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="K5s-RK-Xqf">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" text="Text Views" textAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="K5s-RK-Xqf">
<rect key="frame" x="0.0" y="281.33333333333331" width="353" height="40"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
Expand All @@ -111,8 +111,14 @@
<fontDescription key="fontDescription" type="system" pointSize="30"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Editable:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ckm-Kp-yLh">
<rect key="frame" x="0.0" y="341.33333333333331" width="353" height="14.333333333333314"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="43n-fB-GL1">
<rect key="frame" x="0.0" y="341.33333333333331" width="353" height="128"/>
<rect key="frame" x="0.0" y="375.66666666666669" width="353" height="128"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="128" id="Xep-n3-nDX"/>
Expand All @@ -122,8 +128,14 @@
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" textAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="7DW-74-qEX">
<rect key="frame" x="0.0" y="489.33333333333337" width="353" height="128"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Non-editable:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NFk-Bb-d0Y">
<rect key="frame" x="0.0" y="523.66666666666663" width="353" height="14.333333333333371"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" textAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="7DW-74-qEX">
<rect key="frame" x="0.0" y="558" width="353" height="128"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="128" id="c5V-bh-7NW"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,3 @@ public struct SessionReplayConfiguration {
self.customUploadURL = customUploadURL
}
}

/// Session Replay content recording policy.
/// It describes the way in which sensitive content (e.g. text or images) should be captured.
public enum SessionReplayPrivacy {
/// Record all content as it is.
/// When using this option: all text, images and other information will be recorded and presented in the player.
case allowAll

/// Mask all content.
/// When using this option: all characters in texts will be replaced with "x", images will be
/// replaced with placeholders and other content will be masked accordingly, so the original
/// information will not be presented in the player.
///
/// This is the default content policy.
case maskAll

/// Mask input elements, but record all other content as it is.
/// When uing this option: all user input and selected values (text fields, switches, pickers, segmented controls etc.) will be masked,
/// but static text (e.g. in labels) will be not.
case maskUserInput
}

internal extension SessionReplayPrivacy {
/// If input elements should be masked in this privacy mode.
var shouldMaskInputElements: Bool {
switch self {
case .maskAll, .maskUserInput: return true
case .allowAll: return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,10 @@ internal protocol TextObfuscating {
func mask(text: String) -> String
}

/// Available text obfuscators.
internal struct TextObfuscators {
/// Text obfuscator that returns original text (no obfuscation).
let nop = NOPTextObfuscator()
/// Text obfuscator that replaces each character with `"x"` mask.
let spacePreservingMask = SpacePreservingMaskObfuscator()
/// Text obfuscator that replaces whole text with fixed-length `"xxx"` mask (three asterics).
let fixLegthMask = FixLegthMaskObfuscator()
}

/// Text obfuscator which replaces all readable characters with space-preserving `"x"` characters.
internal struct SpacePreservingMaskObfuscator: TextObfuscating {
/// The character to mask text with.
let maskCharacter: UnicodeScalar = "x"
private static let maskCharacter: UnicodeScalar = "x"

/// Masks given `text` by replacing all not whitespace characters with `"x"`.
/// - Parameter text: the text to be masked
Expand All @@ -41,17 +31,17 @@ internal struct SpacePreservingMaskObfuscator: TextObfuscating {
case " ", "\n", "\r", "\t":
masked.unicodeScalars.append(nextScalar)
default:
masked.unicodeScalars.append(maskCharacter)
masked.unicodeScalars.append(SpacePreservingMaskObfuscator.maskCharacter)
}
}

return masked
}
}

/// Text obfuscator which replaces entire text with fix-length `"xxx"` mask value.
internal struct FixLegthMaskObfuscator: TextObfuscating {
private static let maskedString = "xxx"
/// Text obfuscator which replaces entire text with fix-length `"***"` mask value.
internal struct FixLengthMaskObfuscator: TextObfuscating {
private static let maskedString = "***"

func mask(text: String) -> String { Self.maskedString }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,39 @@ internal extension UIView {
}
}
}

/// Sensitive text content types as defined in Session Replay.
internal let sensitiveContentTypes: Set<UITextContentType> = {
var all: Set<UITextContentType> = [
.password,
.emailAddress,
.telephoneNumber,
.addressCity, .addressState, .addressCityAndState, .fullStreetAddress, .streetAddressLine1, .streetAddressLine2, .postalCode,
.creditCardNumber
]

if #available(iOS 12.0, *) {
all.formUnion([.newPassword, .oneTimeCode])
}

return all
}()

internal extension UITextInputTraits {
/// Whether or not these input traits describe a "Sensitive Text".
///
/// In Session Replay, "Sensitive Text" is:
/// - passwords, e-mails and phone numbers marked in a platform-specific way
/// - AND other forms of sensitivity in text available to each platform
var isSensitiveText: Bool {
if isSecureTextEntry == true {
return true
}

if let contentType = textContentType, let contentType = contentType {
return sensitiveContentTypes.contains(contentType)
}

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,7 @@ private struct WheelsStyleDatePickerRecorder {
nodeRecorders: [
UIPickerViewRecorder(
textObfuscator: { context in
switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.spacePreservingMask
}
return context.recorder.privacy.staticTextObfuscator
}
)
]
Expand All @@ -97,11 +93,7 @@ private struct InlineStyleDatePickerRecorder {
self.viewRecorder = UIViewRecorder()
self.labelRecorder = UILabelRecorder(
textObfuscator: { context in
switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.spacePreservingMask
}
return context.recorder.privacy.staticTextObfuscator
}
)
self.subtreeRecorder = ViewTreeRecorder(
Expand Down Expand Up @@ -144,11 +136,7 @@ private struct CompactStyleDatePickerRecorder {
UIViewRecorder(),
UILabelRecorder(
textObfuscator: { context in
switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.spacePreservingMask
}
return context.recorder.privacy.staticTextObfuscator
}
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ internal class UILabelRecorder: NodeRecorder {
init(
builderOverride: @escaping (UILabelWireframesBuilder) -> UILabelWireframesBuilder = { $0 },
textObfuscator: @escaping (ViewTreeRecordingContext) -> TextObfuscating = { context in
switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.nop
}
return context.recorder.privacy.staticTextObfuscator
}
) {
self.builderOverride = builderOverride
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ internal struct UIPickerViewRecorder: NodeRecorder {

init(
textObfuscator: @escaping (ViewTreeRecordingContext) -> TextObfuscating = { context in
switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.fixLegthMask
case .maskUserInput: return context.textObfuscators.fixLegthMask
}
return context.recorder.privacy.inputAndOptionTextObfuscator
}
) {
self.selectionRecorder = ViewTreeRecorder(nodeRecorders: [UIViewRecorder()])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import UIKit

internal struct UISegmentRecorder: NodeRecorder {
var textObfuscator: (ViewTreeRecordingContext) -> TextObfuscating = { context in
switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.spacePreservingMask
}
return context.recorder.privacy.inputAndOptionTextObfuscator
}

func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal struct UISliderRecorder: NodeRecorder {
thumbWireframeID: ids[3],
value: (min: slider.minimumValue, max: slider.maximumValue, current: slider.value),
isEnabled: slider.isEnabled,
isMasked: context.recorder.privacy.shouldMaskInputElements,
minTrackTintColor: slider.minimumTrackTintColor?.cgColor ?? slider.tintColor?.cgColor,
maxTrackTintColor: slider.maximumTrackTintColor?.cgColor,
thumbTintColor: slider.thumbTintColor?.cgColor
Expand All @@ -46,11 +47,45 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder {
let thumbWireframeID: WireframeID
let value: (min: Float, max: Float, current: Float)
let isEnabled: Bool
let isMasked: Bool
let minTrackTintColor: CGColor?
let maxTrackTintColor: CGColor?
let thumbTintColor: CGColor?

func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] {
if isMasked {
return createMasked(with: builder)
} else {
return createNotMasked(with: builder)
}
}

private func createMasked(with builder: WireframesBuilder) -> [SRWireframe] {
let trackFrame = wireframeRect.divided(atDistance: 3, from: .minYEdge)
.slice
.putInside(wireframeRect, horizontalAlignment: .left, verticalAlignment: .middle)

let track = builder.createShapeWireframe(
id: minTrackWireframeID,
frame: trackFrame,
borderColor: nil,
borderWidth: nil,
backgroundColor: SystemColors.tertiarySystemFill,
cornerRadius: wireframeRect.height * 0.5,
opacity: isEnabled ? attributes.alpha : 0.5
)

// Create background wireframe if the underlying `UIView` has any appearance:
if attributes.hasAnyAppearance {
let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: attributes.frame, attributes: attributes)

return [background, track]
} else {
return [track]
}
}

private func createNotMasked(with builder: WireframesBuilder) -> [SRWireframe] {
guard value.max > value.min else {
return [] // illegal, should not happen
}
Expand Down Expand Up @@ -106,9 +141,9 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder {

if attributes.hasAnyAppearance {
// Create background wireframe only if view declares visible background
let backgorund = builder.createShapeWireframe(id: backgroundWireframeID, frame: wireframeRect, attributes: attributes)
let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: wireframeRect, attributes: attributes)

return [backgorund, leftTrack, rightTrack, thumb]
return [background, leftTrack, rightTrack, thumb]
} else {
return [leftTrack, rightTrack, thumb]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ internal struct UITextFieldRecorder: NodeRecorder {
private let iconsRecorder: UIImageViewRecorder
private let subtreeRecorder: ViewTreeRecorder

var textObfuscator: (ViewTreeRecordingContext, _ isSensitiveText: Bool) -> TextObfuscating = { context, isSensitiveText in
if isSensitiveText {
return context.textObfuscators.fixLegthMask
}

switch context.recorder.privacy {
case .allowAll: return context.textObfuscators.nop
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.fixLegthMask
var textObfuscator: (ViewTreeRecordingContext, _ isSensitive: Bool, _ isPlaceholder: Bool) -> TextObfuscating = { context, isSensitive, isPlaceholder in
if isPlaceholder {
return context.recorder.privacy.hintTextObfuscator
} else if isSensitive {
return context.recorder.privacy.sensitiveTextObfuscator
} else {
return context.recorder.privacy.inputAndOptionTextObfuscator
}
}

Expand Down Expand Up @@ -83,8 +81,6 @@ internal struct UITextFieldRecorder: NodeRecorder {
let textFrame = attributes.frame
.insetBy(dx: 5, dy: 5) // 5 points padding

let isSensitiveText = textField.isSecureTextEntry || textField.textContentType == .emailAddress || textField.textContentType == .telephoneNumber

let builder = UITextFieldWireframesBuilder(
wireframeRect: textFrame,
attributes: attributes,
Expand All @@ -95,7 +91,7 @@ internal struct UITextFieldRecorder: NodeRecorder {
isPlaceholderText: isPlaceholder,
font: textField.font,
fontScalingEnabled: textField.adjustsFontSizeToFitWidth,
textObfuscator: textObfuscator(context, isSensitiveText)
textObfuscator: textObfuscator(context, textField.isSensitiveText, isPlaceholder)
)
return Node(viewAttributes: attributes, wireframesBuilder: builder)
}
Expand Down
Loading

0 comments on commit 91f9146

Please sign in to comment.