LEAccount::createLEAccount()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 3
nop 1
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 5
rs 9.6111
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 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
Documentation Bug introduced by
It seems like $storage of type Zwartpet\PHPCertificateT...AccountStorageInterface is incompatible with the declared type Zwartpet\PHPCertificateT...ificateStorageInterface of property $storage.

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..

Loading history...
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']);
0 ignored issues
show
Bug introduced by
The method setAccountPublicKey() does not exist on Zwartpet\PHPCertificateT...ificateStorageInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

215
        $this->storage->/** @scrutinizer ignore-call */ 
216
                        setAccountPublicKey($new['public']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
216 2
        $this->storage->setAccountPrivateKey($new['private']);
0 ignored issues
show
Bug introduced by
The method setAccountPrivateKey() does not exist on Zwartpet\PHPCertificateT...ificateStorageInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

216
        $this->storage->/** @scrutinizer ignore-call */ 
217
                        setAccountPrivateKey($new['private']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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