1 | <?php |
||
2 | namespace Zwartpet\PHPCertificateToolbox; |
||
3 | |||
4 | use Zwartpet\PHPCertificateToolbox\Exception\RuntimeException; |
||
5 | use Psr\Log\LoggerInterface; |
||
6 | |||
7 | /** |
||
8 | * LetsEncrypt Account class, containing the functions and data associated with a LetsEncrypt account. |
||
9 | * |
||
10 | * @author Youri van Weegberg <[email protected]> |
||
11 | * @copyright 2018 Youri van Weegberg |
||
12 | * @license https://opensource.org/licenses/mit-license.php MIT License |
||
13 | */ |
||
14 | class LEAccount |
||
15 | { |
||
16 | private $connector; |
||
17 | |||
18 | public $id; |
||
19 | public $key; |
||
20 | public $contact; |
||
21 | public $agreement; |
||
22 | public $initialIp; |
||
23 | public $createdAt; |
||
24 | public $status; |
||
25 | |||
26 | /** @var LoggerInterface */ |
||
27 | private $log; |
||
28 | |||
29 | /** @var CertificateStorageInterface */ |
||
30 | private $storage; |
||
31 | |||
32 | /** |
||
33 | * Initiates the LetsEncrypt Account class. |
||
34 | * |
||
35 | * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests. |
||
36 | * @param LoggerInterface $log PSR-3 compatible logger |
||
37 | * @param array $email The array of strings containing e-mail addresses. Only used when creating a |
||
38 | * new account. |
||
39 | * @param AccountStorageInterface $storage storage for account keys |
||
40 | */ |
||
41 | 12 | public function __construct($connector, LoggerInterface $log, $email, AccountStorageInterface $storage) |
|
42 | { |
||
43 | 12 | $this->connector = $connector; |
|
44 | 12 | $this->storage = $storage; |
|
0 ignored issues
–
show
|
|||
45 | 12 | $this->log = $log; |
|
46 | |||
47 | 12 | if (empty($storage->getAccountPublicKey()) || empty($storage->getAccountPrivateKey())) { |
|
48 | 12 | $this->log->notice("No account found for ".implode(',', $email).", attempting to create account"); |
|
49 | |||
50 | 12 | $accountKey = LEFunctions::RSAgenerateKeys(); |
|
51 | 12 | $storage->setAccountPublicKey($accountKey['public']); |
|
52 | 12 | $storage->setAccountPrivateKey($accountKey['private']); |
|
53 | |||
54 | 12 | $this->connector->accountURL = $this->createLEAccount($email); |
|
55 | } else { |
||
56 | 4 | $this->connector->accountURL = $this->getLEAccount(); |
|
57 | } |
||
58 | 12 | if ($this->connector->accountURL === false) { |
|
59 | 2 | throw new RuntimeException('Account not found or deactivated.'); |
|
60 | } |
||
61 | 12 | $this->getLEAccountData(); |
|
62 | 12 | } |
|
63 | |||
64 | /** |
||
65 | * Creates a new LetsEncrypt account. |
||
66 | * |
||
67 | * @param array $email The array of strings containing e-mail addresses. |
||
68 | * |
||
69 | * @return string|bool Returns the new account URL when the account was successfully created, false if not. |
||
70 | */ |
||
71 | private function createLEAccount($email) |
||
72 | { |
||
73 | 12 | $contact = array_map(function ($addr) { |
|
74 | 12 | return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr); |
|
75 | 12 | }, $email); |
|
76 | |||
77 | 12 | $sign = $this->connector->signRequestJWK( |
|
78 | 12 | ['contact' => $contact, 'termsOfServiceAgreed' => true], |
|
79 | 12 | $this->connector->newAccount |
|
80 | ); |
||
81 | 12 | $post = $this->connector->post($this->connector->newAccount, $sign); |
|
82 | 12 | if (strpos($post['header'], "201 Created") !== false) { |
|
83 | 12 | if (preg_match('~Location: (\S+)~i', $post['header'], $matches)) { |
|
84 | 12 | return trim($matches[1]); |
|
85 | } |
||
86 | } |
||
87 | //@codeCoverageIgnoreStart |
||
88 | return false; |
||
89 | //@codeCoverageIgnoreEnd |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * Gets the LetsEncrypt account URL associated with the stored account keys. |
||
94 | * |
||
95 | * @return string|bool Returns the account URL if it is found, or false when none is found. |
||
96 | */ |
||
97 | 4 | private function getLEAccount() |
|
98 | { |
||
99 | 4 | $sign = $this->connector->signRequestJWK(['onlyReturnExisting' => true], $this->connector->newAccount); |
|
100 | 4 | $post = $this->connector->post($this->connector->newAccount, $sign); |
|
101 | |||
102 | 4 | if (strpos($post['header'], "200 OK") !== false) { |
|
103 | 2 | if (preg_match('~Location: (\S+)~i', $post['header'], $matches)) { |
|
104 | 2 | return trim($matches[1]); |
|
105 | } |
||
106 | } |
||
107 | 2 | return false; |
|
108 | } |
||
109 | |||
110 | /** |
||
111 | * Gets the LetsEncrypt account data from the account URL. |
||
112 | */ |
||
113 | 12 | private function getLEAccountData() |
|
114 | { |
||
115 | 12 | $sign = $this->connector->signRequestKid( |
|
116 | 12 | ['' => ''], |
|
117 | 12 | $this->connector->accountURL, |
|
118 | 12 | $this->connector->accountURL |
|
119 | ); |
||
120 | 12 | $post = $this->connector->post($this->connector->accountURL, $sign); |
|
121 | 12 | if (strpos($post['header'], "200 OK") !== false) { |
|
122 | 12 | $this->id = isset($post['body']['id']) ? $post['body']['id'] : ''; |
|
123 | 12 | $this->key = $post['body']['key']; |
|
124 | 12 | $this->contact = $post['body']['contact']; |
|
125 | 12 | $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : null; |
|
126 | 12 | $this->initialIp = $post['body']['initialIp']; |
|
127 | 12 | $this->createdAt = $post['body']['createdAt']; |
|
128 | 12 | $this->status = $post['body']['status']; |
|
129 | } else { |
||
130 | //@codeCoverageIgnoreStart |
||
131 | throw new RuntimeException('Account data cannot be found.'); |
||
132 | //@codeCoverageIgnoreEnd |
||
133 | } |
||
134 | 12 | } |
|
135 | |||
136 | /** |
||
137 | * Updates account data. Now just supporting new contact information. |
||
138 | * |
||
139 | * @param array $email The array of strings containing e-mail adresses. |
||
140 | * |
||
141 | * @return boolean Returns true if the update is successful, false if not. |
||
142 | */ |
||
143 | public function updateAccount($email) |
||
144 | { |
||
145 | 2 | $contact = array_map(function ($addr) { |
|
146 | 2 | return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr); |
|
147 | 2 | }, $email); |
|
148 | |||
149 | 2 | $sign = $this->connector->signRequestKid( |
|
150 | 2 | ['contact' => $contact], |
|
151 | 2 | $this->connector->accountURL, |
|
152 | 2 | $this->connector->accountURL |
|
153 | ); |
||
154 | 2 | $post = $this->connector->post($this->connector->accountURL, $sign); |
|
155 | 2 | if ($post['status'] !== 200) { |
|
156 | //@codeCoverageIgnoreStart |
||
157 | throw new RuntimeException('Unable to update account'); |
||
158 | //@codeCoverageIgnoreEnd |
||
159 | } |
||
160 | |||
161 | 2 | $this->id = isset($post['body']['id']) ? $post['body']['id'] : ''; |
|
162 | 2 | $this->key = $post['body']['key']; |
|
163 | 2 | $this->contact = $post['body']['contact']; |
|
164 | 2 | $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : ''; |
|
165 | 2 | $this->initialIp = $post['body']['initialIp']; |
|
166 | 2 | $this->createdAt = $post['body']['createdAt']; |
|
167 | 2 | $this->status = $post['body']['status']; |
|
168 | |||
169 | 2 | $this->log->notice('Account data updated'); |
|
170 | 2 | return true; |
|
171 | } |
||
172 | |||
173 | /** |
||
174 | * Creates new RSA account keys and updates the keys with LetsEncrypt. |
||
175 | * |
||
176 | * @return boolean Returns true if the update is successful, false if not. |
||
177 | */ |
||
178 | 2 | public function changeAccountKeys() |
|
179 | { |
||
180 | 2 | $new=LEFunctions::RSAgenerateKeys(); |
|
181 | |||
182 | 2 | $privateKey = openssl_pkey_get_private($new['private']); |
|
183 | 2 | if ($privateKey === false) { |
|
184 | //@codeCoverageIgnoreStart |
||
185 | throw new RuntimeException('Failed to open newly generated private key'); |
||
186 | //@codeCoverageIgnoreEnd |
||
187 | } |
||
188 | |||
189 | |||
190 | 2 | $details = openssl_pkey_get_details($privateKey); |
|
191 | 2 | $innerPayload = ['account' => $this->connector->accountURL, 'newKey' => [ |
|
192 | 2 | "kty" => "RSA", |
|
193 | 2 | "n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"]), |
|
194 | 2 | "e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"]) |
|
195 | ]]; |
||
196 | 2 | $outerPayload = $this->connector->signRequestJWK( |
|
197 | 2 | $innerPayload, |
|
198 | 2 | $this->connector->keyChange, |
|
199 | 2 | $new['private'] |
|
200 | ); |
||
201 | 2 | $sign = $this->connector->signRequestKid( |
|
202 | 2 | $outerPayload, |
|
203 | 2 | $this->connector->accountURL, |
|
204 | 2 | $this->connector->keyChange |
|
205 | ); |
||
206 | 2 | $post = $this->connector->post($this->connector->keyChange, $sign); |
|
207 | 2 | if ($post['status'] !== 200) { |
|
208 | //@codeCoverageIgnoreStart |
||
209 | throw new RuntimeException('Unable to post new account keys'); |
||
210 | //@codeCoverageIgnoreEnd |
||
211 | } |
||
212 | |||
213 | 2 | $this->getLEAccountData(); |
|
214 | |||
215 | 2 | $this->storage->setAccountPublicKey($new['public']); |
|
216 | 2 | $this->storage->setAccountPrivateKey($new['private']); |
|
217 | |||
218 | 2 | $this->log->notice('Account keys changed'); |
|
219 | 2 | return true; |
|
220 | } |
||
221 | |||
222 | /** |
||
223 | * Deactivates the LetsEncrypt account. |
||
224 | * |
||
225 | * @return boolean Returns true if the deactivation is successful, false if not. |
||
226 | */ |
||
227 | 2 | public function deactivateAccount() |
|
228 | { |
||
229 | 2 | $sign = $this->connector->signRequestKid( |
|
230 | 2 | ['status' => 'deactivated'], |
|
231 | 2 | $this->connector->accountURL, |
|
232 | 2 | $this->connector->accountURL |
|
233 | ); |
||
234 | 2 | $post = $this->connector->post($this->connector->accountURL, $sign); |
|
235 | 2 | if ($post['status'] !== 200) { |
|
236 | //@codeCoverageIgnoreStart |
||
237 | $this->log->error('Account deactivation failed'); |
||
238 | return false; |
||
239 | //@codeCoverageIgnoreEnd |
||
240 | } |
||
241 | |||
242 | 2 | $this->connector->accountDeactivated = true; |
|
243 | 2 | $this->log->info('Account deactivated'); |
|
244 | 2 | return true; |
|
245 | } |
||
246 | } |
||
247 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..