1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2024-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: */
21:
22: namespace SimpleID\Crypt;
23:
24: use \Stringable;
25: use Branca\Branca;
26: use Symfony\Component\Yaml\Tag\TaggedValue;
27:
28: /**
29: * A secure string that is encrypted at rest.
30: *
31: * The string is encrypted as a {@link Branca\Branca} token when stored.
32: * The key is obtained from the `SIMPLEID_SECURE_SECRET` environment
33: * variable (as a binary string), or a file specified by the
34: * `SIMPLEID_SECURE_SECRET_FILE` environment variable.
35: */
36: final class SecureString implements \Stringable {
37: /** @var string */
38: private $ciphertext;
39:
40: /**
41: * Creates a SecureString from the specified ciphertext
42: *
43: * @param string $ciphertext the ciphertext
44: */
45: public function __construct(string $ciphertext) {
46: $this->ciphertext = $ciphertext;
47: }
48:
49: /**
50: * Creates a SecureString from a plaintext string
51: *
52: * @param string $plaintext the plaintext to encrypt
53: * @return SecureString the secure string
54: */
55: static public function fromPlaintext(string $plaintext): SecureString {
56: $branca = new Branca(self::getKey());
57: return new SecureString($branca->encode($plaintext));
58: }
59:
60: /**
61: * Returns the plaintext version of the secure string
62: *
63: * @return string the plaintext
64: * @throws \RuntimeException if an error occurs during the decryption process
65: */
66: public function toPlaintext(): string {
67: $branca = new Branca(self::getKey());
68: return $branca->decode($this->ciphertext);
69: }
70:
71: /**
72: * Returns a YAML tagged value for serialisation into YAML
73: *
74: * @return TaggedValue the YAML tagged value
75: */
76: public function toYamlTaggedValue(): TaggedValue {
77: return new TaggedValue('secure_string', $this->ciphertext);
78: }
79:
80: /**
81: * Returns the plain text value.
82: *
83: * The plaint text value is determined based on the following rules:
84: *
85: * 1. If $value is a SecureString, then the value is decrypted
86: * 2. If $value is a YAML !secure_string tagged value, then a SecureString is
87: * created from the YAML value and is decrypted
88: * 3. If $value is or can be converted to a string, then the string value
89: * is returned
90: * 4. If $value is null, then null is returned
91: * 5. Otherwise an InvalidArugmentException is raised
92: *
93: * @param SecureString|TaggedValue|string|Stringable|null $value the value to
94: * get the plain text value
95: * @return string the plain text value, or null if $value is null
96: * @throws \InvalidArgumentException
97: */
98: static public function getPlaintext($value): ?string {
99: if ($value instanceof SecureString) {
100: return $value->toPlaintext();
101: } elseif (($value instanceof TaggedValue) && ($value->getTag() == 'secure_string')) {
102: return (new SecureString($value->getValue()))->toPlaintext();
103: } elseif (is_string($value)) {
104: return $value;
105: } elseif ($value instanceof Stringable) {
106: return $value->__toString();
107: } elseif ($value == null) {
108: return null;
109: }
110:
111: throw new \InvalidArgumentException();
112: }
113:
114: /**
115: * {@inheritdoc}
116: */
117: public function __toString(): string {
118: return $this->ciphertext;
119: }
120:
121: /**
122: * {@inheritdoc}
123: */
124: public function __serialize(): array {
125: return ['ciphertext' => $this->ciphertext ];
126: }
127:
128: /**
129: * {@inheritdoc}
130: * @param array<mixed> $data
131: * @return void
132: */
133: public function __unserialize(array $data) {
134: $this->ciphertext = $data['ciphertext'];
135: }
136:
137: static private function getKey(): string {
138: /** @var ?string */
139: static $key = null;
140:
141: if ($key == null) {
142: if (isset($_ENV['SIMPLEID_SECURE_SECRET'])) {
143: $secret = $_ENV['SIMPLEID_SECURE_SECRET'];
144: } elseif (isset($_ENV['SIMPLEID_SECURE_SECRET_FILE'])) {
145: $secret = file_get_contents($_ENV['SIMPLEID_SECURE_SECRET_FILE']);
146: if ($secret === false)
147: throw new \RuntimeException('Error reading file File specified by SIMPLEID_SECURE_SECRET_FILE');
148: } else {
149: throw new \RuntimeException('Key not found');
150: }
151: $key = hash('sha256', $secret, true);
152: }
153:
154: return $key;
155: }
156: }
157:
158: ?>
159: