Skip to content

Commit c84bd85

Browse files
committed
Add Secret
1 parent 8e39459 commit c84bd85

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

src/Nexus/Encryption/Secret.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Encryption;
15+
16+
/**
17+
* An object representation for a secret. This encapsulates sensitive strings
18+
* in a read-only object that forbids serialisation and dumping.
19+
*
20+
* This should be used for:
21+
* - secret keys
22+
* - plaintext (before encryption)
23+
* - plaintext (after decryption)
24+
*/
25+
final class Secret
26+
{
27+
/**
28+
* @throws \RuntimeException
29+
*/
30+
public function __construct(
31+
#[\SensitiveParameter]
32+
private ?string $value,
33+
) {
34+
if (null === $this->value) {
35+
throw new \RuntimeException('Secret cannot accept null.');
36+
}
37+
}
38+
39+
public function __destruct()
40+
{
41+
sodium_memzero($this->value);
42+
}
43+
44+
/**
45+
* @throws \BadMethodCallException
46+
*/
47+
public function __serialize(): never
48+
{
49+
throw new \BadMethodCallException('Cannot serialise a Secret object.');
50+
}
51+
52+
/**
53+
* @return array{value: string}
54+
*/
55+
public function __debugInfo(): array
56+
{
57+
return ['value' => '[redacted]'];
58+
}
59+
60+
public function equals(self $other): bool
61+
{
62+
return hash_equals($this->reveal(), $other->reveal());
63+
}
64+
65+
public function reveal(): string
66+
{
67+
return $this->value ?? '';
68+
}
69+
}

tests/Encryption/SecretTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Nexus\Tests\Encryption;
15+
16+
use Nexus\Encryption\Encoding\HexEncoder;
17+
use Nexus\Encryption\Secret;
18+
use PHPUnit\Framework\Attributes\CoversClass;
19+
use PHPUnit\Framework\Attributes\Group;
20+
use PHPUnit\Framework\TestCase;
21+
22+
/**
23+
* @internal
24+
*/
25+
#[CoversClass(Secret::class)]
26+
#[Group('unit-test')]
27+
final class SecretTest extends TestCase
28+
{
29+
private string $data;
30+
31+
protected function setUp(): void
32+
{
33+
$this->data = (new HexEncoder())->encode(random_bytes(32));
34+
}
35+
36+
public function testCannotAcceptNullOnConstruct(): void
37+
{
38+
$this->expectException(\RuntimeException::class);
39+
$this->expectExceptionMessage('Secret cannot accept null.');
40+
41+
new Secret(null);
42+
}
43+
44+
public function testCannotSerialise(): void
45+
{
46+
$this->expectException(\BadMethodCallException::class);
47+
$this->expectExceptionMessage('Cannot serialise a Secret object.');
48+
49+
serialize(new Secret($this->data));
50+
}
51+
52+
public function testCannotDumpString(): void
53+
{
54+
$plaintext = new Secret($this->data);
55+
56+
ob_start();
57+
var_dump($plaintext);
58+
$dump = (string) ob_get_clean();
59+
$dump = preg_replace('/(\033\[[0-9;]+m)|(\035\[[0-9;]+m)/u', '', $dump) ?? $dump;
60+
$print = print_r($plaintext, true);
61+
62+
self::assertStringNotContainsString($this->data, $dump);
63+
self::assertStringContainsString('[redacted]', $dump);
64+
self::assertStringNotContainsString($this->data, $print);
65+
self::assertStringContainsString('[redacted]', $print);
66+
}
67+
68+
public function testEquals(): void
69+
{
70+
$one = new Secret($this->data);
71+
$two = new Secret($this->data);
72+
$three = new Secret((new HexEncoder())->encode(random_bytes(32)));
73+
74+
self::assertTrue($one->equals($two));
75+
self::assertFalse($one->equals($three));
76+
self::assertTrue($two->equals($one));
77+
self::assertFalse($two->equals($three));
78+
self::assertFalse($three->equals($one));
79+
self::assertFalse($three->equals($two));
80+
}
81+
82+
public function testRevealNeverReturnsEmptyString(): void
83+
{
84+
$plaintext = new Secret($this->data);
85+
86+
self::assertSame($this->data, $plaintext->reveal());
87+
}
88+
}

0 commit comments

Comments
 (0)