Passed
Pull Request — master (#1)
by John
02:11
created

LEAccount::updateAccount()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 5
nop 1
dl 0
loc 28
rs 9.0111
c 0
b 0
f 0
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 CertificateStorageInterface $storage  storage for account keys
40
     */
41
    public function __construct($connector, LoggerInterface $log, $email, CertificateStorageInterface $storage)
42
    {
43
        $this->connector = $connector;
44
        $this->storage = $storage;
45
        $this->log = $log;
46
47
        if (empty($storage->getAccountPublicKey()) || empty($storage->getAccountPrivateKey())) {
48
            $this->log->notice("No account found for ".implode(',', $email).", attempting to create account");
49
50
            $accountKey = LEFunctions::RSAgenerateKeys();
51
            $storage->setAccountPublicKey($accountKey['public']);
52
            $storage->setAccountPrivateKey($accountKey['private']);
53
54
            $this->connector->accountURL = $this->createLEAccount($email);
55
        } else {
56
            $this->connector->accountURL = $this->getLEAccount();
57
        }
58
        if ($this->connector->accountURL === false) {
59
            throw new RuntimeException('Account not found or deactivated.');
60
        }
61
        $this->getLEAccountData();
62
    }
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
        $contact = array_map(function ($addr) {
74
            return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr);
75
        }, $email);
76
77
        $sign = $this->connector->signRequestJWK(
78
            ['contact' => $contact, 'termsOfServiceAgreed' => true],
79
            $this->connector->newAccount
80
        );
81
        $post = $this->connector->post($this->connector->newAccount, $sign);
82
        if (strpos($post['header'], "201 Created") !== false) {
83
            if (preg_match('~Location: (\S+)~i', $post['header'], $matches)) {
84
                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
    private function getLEAccount()
98
    {
99
        $sign = $this->connector->signRequestJWK(['onlyReturnExisting' => true], $this->connector->newAccount);
100
        $post = $this->connector->post($this->connector->newAccount, $sign);
101
102
        if (strpos($post['header'], "200 OK") !== false) {
103
            if (preg_match('~Location: (\S+)~i', $post['header'], $matches)) {
104
                return trim($matches[1]);
105
            }
106
        }
107
        return false;
108
    }
109
110
    /**
111
     * Gets the LetsEncrypt account data from the account URL.
112
     */
113
    private function getLEAccountData()
114
    {
115
        $sign = $this->connector->signRequestKid(
116
            ['' => ''],
117
            $this->connector->accountURL,
118
            $this->connector->accountURL
119
        );
120
        $post = $this->connector->post($this->connector->accountURL, $sign);
121
        if (strpos($post['header'], "200 OK") !== false) {
122
            $this->id = isset($post['body']['id']) ? $post['body']['id'] : '';
123
            $this->key = $post['body']['key'];
124
            $this->contact = $post['body']['contact'];
125
            $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : null;
126
            $this->initialIp = $post['body']['initialIp'];
127
            $this->createdAt = $post['body']['createdAt'];
128
            $this->status = $post['body']['status'];
129
        } else {
130
            //@codeCoverageIgnoreStart
131
            throw new RuntimeException('Account data cannot be found.');
132
            //@codeCoverageIgnoreEnd
133
        }
134
    }
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
        $contact = array_map(function ($addr) {
146
            return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr);
147
        }, $email);
148
149
        $sign = $this->connector->signRequestKid(
150
            ['contact' => $contact],
151
            $this->connector->accountURL,
152
            $this->connector->accountURL
153
        );
154
        $post = $this->connector->post($this->connector->accountURL, $sign);
155
        if ($post['status'] !== 200) {
156
            //@codeCoverageIgnoreStart
157
            throw new RuntimeException('Unable to update account');
158
            //@codeCoverageIgnoreEnd
159
        }
160
161
        $this->id = isset($post['body']['id']) ? $post['body']['id'] : '';
162
        $this->key = $post['body']['key'];
163
        $this->contact = $post['body']['contact'];
164
        $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : '';
165
        $this->initialIp = $post['body']['initialIp'];
166
        $this->createdAt = $post['body']['createdAt'];
167
        $this->status = $post['body']['status'];
168
169
        $this->log->notice('Account data updated');
170
        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
    public function changeAccountKeys()
179
    {
180
        $new=LEFunctions::RSAgenerateKeys();
181
182
        $privateKey = openssl_pkey_get_private($new['private']);
183
        if ($privateKey === false) {
184
            //@codeCoverageIgnoreStart
185
            throw new RuntimeException('Failed to open newly generated private key');
186
            //@codeCoverageIgnoreEnd
187
        }
188
189
190
        $details = openssl_pkey_get_details($privateKey);
191
        $innerPayload = ['account' => $this->connector->accountURL, 'newKey' => [
192
            "kty" => "RSA",
193
            "n" => LEFunctions::base64UrlSafeEncode($details["rsa"]["n"]),
194
            "e" => LEFunctions::base64UrlSafeEncode($details["rsa"]["e"])
195
        ]];
196
        $outerPayload = $this->connector->signRequestJWK(
197
            $innerPayload,
198
            $this->connector->keyChange,
199
            $new['private']
200
        );
201
        $sign = $this->connector->signRequestKid(
202
            $outerPayload,
203
            $this->connector->accountURL,
204
            $this->connector->keyChange
205
        );
206
        $post = $this->connector->post($this->connector->keyChange, $sign);
207
        if ($post['status'] !== 200) {
208
            //@codeCoverageIgnoreStart
209
            throw new RuntimeException('Unable to post new account keys');
210
            //@codeCoverageIgnoreEnd
211
        }
212
213
        $this->getLEAccountData();
214
215
        $this->storage->setAccountPublicKey($new['public']);
216
        $this->storage->setAccountPrivateKey($new['private']);
217
218
        $this->log->notice('Account keys changed');
219
        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
    public function deactivateAccount()
228
    {
229
        $sign = $this->connector->signRequestKid(
230
            ['status' => 'deactivated'],
231
            $this->connector->accountURL,
232
            $this->connector->accountURL
233
        );
234
        $post = $this->connector->post($this->connector->accountURL, $sign);
235
        if ($post['status'] !== 200) {
236
            //@codeCoverageIgnoreStart
237
            $this->log->error('Account deactivation failed');
238
            return false;
239
            //@codeCoverageIgnoreEnd
240
        }
241
242
        $this->connector->accountDeactivated = true;
243
        $this->log->info('Account deactivated');
244
        return true;
245
    }
246
}
247