Skip to content

Commit 9d0f402

Browse files
authored
Release/1.2.0 (#4)
1 parent f4e8eb8 commit 9d0f402

File tree

4 files changed

+63
-14
lines changed

4 files changed

+63
-14
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ PhoneRedaction::from(fields: ['phone', 'mobile', 'whatsapp'], visibleSuffixLengt
172172

173173
#### Password redaction
174174

175-
Masks the entire value. No characters are preserved.
175+
Masks the entire value with a fixed-length mask (default: 8 characters). The original value's length is never revealed
176+
in the output, preventing information leakage about password size.
176177

177178
```php
178179
use TinyBlocks\Logger\StructuredLogger;
@@ -184,15 +185,21 @@ $logger = StructuredLogger::create()
184185
->build();
185186

186187
$logger->info(message: 'login.attempt', context: ['password' => 's3cr3t!']);
187-
# password → "*******"
188+
# password → "********"
189+
190+
$logger->info(message: 'login.attempt', context: ['password' => '123']);
191+
# password → "********" (same mask regardless of length)
188192
```
189193

190-
With custom fields:
194+
With custom fields and fixed mask length:
191195

192196
```php
193197
use TinyBlocks\Logger\Redactions\PasswordRedaction;
194198

195-
PasswordRedaction::from(fields: ['password', 'secret', 'token']);
199+
PasswordRedaction::from(fields: ['password', 'secret', 'token'], fixedMaskLength: 12);
200+
# "s3cr3t!" → "************"
201+
# "ab" → "************"
202+
# "long_password" → "************"
196203
```
197204

198205
#### Name redaction

src/Redactions/EmailRedaction.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ private function __construct(array $fields, int $visiblePrefixLength)
2424
return str_repeat('*', strlen($value));
2525
}
2626

27-
$localPart = substr($value, 0, $atPosition);
2827
$domain = substr($value, $atPosition);
29-
$visiblePrefix = substr($localPart, 0, $visiblePrefixLength);
28+
$localPart = substr($value, 0, $atPosition);
3029
$maskedSuffix = str_repeat('*', max(0, strlen($localPart) - $visiblePrefixLength));
30+
$visiblePrefix = substr($localPart, 0, $visiblePrefixLength);
3131

3232
return sprintf('%s%s%s', $visiblePrefix, $maskedSuffix, $domain);
3333
}

src/Redactions/PasswordRedaction.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@
99

1010
final readonly class PasswordRedaction implements Redaction
1111
{
12+
private const int DEFAULT_FIXED_MASK_LENGTH = 8;
13+
1214
private Redactor $redactor;
1315

14-
private function __construct(array $fields)
16+
private function __construct(array $fields, int $fixedMaskLength)
1517
{
1618
$this->redactor = new Redactor(
1719
fields: $fields,
18-
maskingFunction: static function (string $value): string {
19-
return str_repeat('*', strlen($value));
20-
}
20+
maskingFunction: static fn(): string => str_repeat('*', $fixedMaskLength)
2121
);
2222
}
2323

24-
public static function from(array $fields): PasswordRedaction
25-
{
26-
return new PasswordRedaction(fields: $fields);
24+
public static function from(
25+
array $fields,
26+
int $fixedMaskLength = self::DEFAULT_FIXED_MASK_LENGTH
27+
): PasswordRedaction {
28+
return new PasswordRedaction(fields: $fields, fixedMaskLength: $fixedMaskLength);
2729
}
2830

2931
public static function default(): PasswordRedaction

tests/StructuredLoggerTest.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ public function testLogWithPasswordRedactionOnShortValue(): void
528528
/** @Then the password should still be fully masked */
529529
$output = $this->streamContents();
530530

531-
self::assertStringContainsString('**', $output);
531+
self::assertStringContainsString('********', $output);
532532
self::assertStringNotContainsString('"password":"ab"', $output);
533533
}
534534

@@ -556,6 +556,46 @@ public function testLogWithPasswordRedactionOnNestedField(): void
556556
self::assertStringContainsString('"username":"admin"', $output);
557557
}
558558

559+
560+
public function testLogWithPasswordRedactionDoesNotRevealValueLength(): void
561+
{
562+
/** @Given a structured logger with password redaction */
563+
$logger = StructuredLogger::create()
564+
->withStream(stream: $this->stream)
565+
->withComponent(component: 'auth-service')
566+
->withRedactions(PasswordRedaction::default())
567+
->build();
568+
569+
/** @When logging passwords of different lengths */
570+
$logger->info(message: 'login.short', context: ['password' => '123']);
571+
$logger->info(message: 'login.long', context: ['password' => 'mySuperLongP@ssw0rd!123']);
572+
573+
/** @Then both should produce the same fixed-length mask */
574+
$lines = array_filter(explode("\n", $this->streamContents()));
575+
576+
self::assertStringContainsString('"password":"********"', $lines[0]);
577+
self::assertStringContainsString('"password":"********"', $lines[1]);
578+
}
579+
580+
public function testLogWithPasswordRedactionWithCustomFixedMaskLength(): void
581+
{
582+
/** @Given a structured logger with password redaction configured with a custom fixed mask length */
583+
$logger = StructuredLogger::create()
584+
->withStream(stream: $this->stream)
585+
->withComponent(component: 'auth-service')
586+
->withRedactions(PasswordRedaction::from(fields: ['password'], fixedMaskLength: 12))
587+
->build();
588+
589+
/** @When logging with a password field */
590+
$logger->info(message: 'login.attempt', context: ['password' => 'abc']);
591+
592+
/** @Then the mask should have exactly 12 asterisks */
593+
$output = $this->streamContents();
594+
595+
self::assertStringContainsString('"password":"************"', $output);
596+
self::assertStringNotContainsString('abc', $output);
597+
}
598+
559599
public function testLogWithNameRedaction(): void
560600
{
561601
/** @Given a structured logger with name redaction */

0 commit comments

Comments
 (0)