PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / feedback / SensitiveValueMask.php

SensitiveValueMask.php in 404 Solution trunk, at includes/feedback/SensitiveValueMask.php

133 lines 4.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Turns a sensitive value into a stable, partially-revealing placeholder.
9 *
10 * The output format is a contract, not an implementation detail: support
11 * reads these placeholders in debug logs and crash reports, so they must
12 * (a) reveal enough of the head of the value to recognize what KIND of thing
13 * it was, (b) reveal proportionally less of a short value than a long one,
14 * since a short one is nearly guessable from any prefix, and (c) end in a
15 * salted hash so that two occurrences of the same value can be correlated
16 * across lines while remaining unrecoverable. AUTH_SALT makes that hash
17 * site-specific, so a value cannot be looked up from a rainbow table or
18 * matched across two different sites' reports.
19 *
20 * Pure value shaping: no patterns, no I/O, no state. Deciding WHICH runs of
21 * text are sensitive belongs to ABJ_404_Solution_PiiRedactor; this class only
22 * decides what a sensitive value looks like once found. It is the adaptive
23 * counterpart of ABJ_404_Solution_Functions::md5lastOctet(), which does the
24 * same job for a network address.
25 */
26 final class ABJ_404_Solution_SensitiveValueMask {
27
28 /**
29 * Trailing labels that are a registry suffix rather than the name the
30 * registrant chose, so the label before them is the identifying one and
31 * must be masked too: 'co.uk', 'com.au' and friends.
32 *
33 * @var array<int, string>
34 */
35 const MULTI_LABEL_SUFFIX_TLDS = array('uk', 'au', 'nz', 'za');
36
37 /**
38 * Mask an email address, keeping its shape ('ad***@exa***-e64c') so a
39 * reader can still tell an address from a username in the log line.
40 *
41 * The public suffix is dropped rather than masked: '.com' identifies
42 * nobody, and keeping it would spend visible characters on a constant.
43 *
44 * @param string $email
45 * @return string
46 */
47 public static function maskEmail($email) {
48 if (empty($email) || strpos($email, '@') === false) {
49 return $email;
50 }
51
52 $parts = explode('@', $email);
53 if (count($parts) != 2) {
54 return self::maskText($email);
55 }
56
57 list($username, $fullDomain) = $parts;
58
59 $domainParts = explode('.', $fullDomain);
60 if (count($domainParts) > 1) {
61 if (in_array(end($domainParts), self::MULTI_LABEL_SUFFIX_TLDS)) {
62 array_pop($domainParts);
63 array_pop($domainParts);
64 } else {
65 array_pop($domainParts);
66 }
67 }
68 $domain = implode('.', $domainParts);
69
70 $maskedUsername = substr($username, 0, self::visibleCharacters(strlen($username))) . '***';
71
72 $domainVisible = max(1, (int)ceil(strlen($domain) * 0.3));
73 $maskedDomain = empty($domain) ? '' : substr($domain, 0, $domainVisible) . '***';
74
75 if (!empty($maskedDomain)) {
76 return $maskedUsername . '@' . $maskedDomain . '-' . self::correlationHash($email);
77 }
78 return $maskedUsername . '@-' . self::correlationHash($email);
79 }
80
81 /**
82 * Mask an arbitrary sensitive string (a username, a display name, a
83 * database account name).
84 *
85 * @param string $text
86 * @return string
87 */
88 public static function maskText($text) {
89 if (empty($text)) {
90 return $text;
91 }
92
93 $text = trim($text);
94 $masked = substr($text, 0, self::visibleCharacters(strlen($text))) . '***';
95
96 return $masked . '-' . self::correlationHash($text);
97 }
98
99 /**
100 * How much of a value's head may stay readable. A four-character value
101 * gives away almost everything from two characters, so short values
102 * reveal less.
103 *
104 * @param int $length
105 * @return int
106 */
107 private static function visibleCharacters(int $length): int {
108 if ($length <= 4) {
109 return 1;
110 }
111 if ($length <= 9) {
112 return 2;
113 }
114 return 3;
115 }
116
117 /**
118 * Site-specific, non-reversible tail that makes two occurrences of the
119 * same value comparable. Falls back to an unsalted hash before
120 * wp-config.php's salts are available (very early boot, and the plugin's
121 * own test bootstrap), which keeps the value masked either way.
122 *
123 * @param string $value
124 * @return string
125 */
126 private static function correlationHash(string $value): string {
127 if (defined('AUTH_SALT')) {
128 return substr(md5(AUTH_SALT . $value), 0, 4);
129 }
130 return substr(md5($value), 0, 4);
131 }
132 }
133