Issues (11)

src/IronBox.php (4 issues)

1
<?php
2
3
namespace SGP\IronBox;
4
5
use GuzzleHttp\Client;
6
use SGP\IronBox\Encryption\FileEncrypter;
7
use SGP\IronBox\Enums\EntityTypes;
8
use SGP\IronBox\Exceptions\FileNotFound;
9
use SGP\IronBox\Exceptions\IronBoxException;
10
use SGP\IronBox\Exceptions\ServiceUnavailable;
11
12
class IronBox
13
{
14
    /**
15
     * @var \GuzzleHttp\Client
16
     */
17
    protected $client;
18
19
    /**
20
     * @var array
21
     */
22
    protected $httpHeaders = [
23
        'Accept' => 'application/json',
24
    ];
25
26
    /**
27
     * @var string
28
     */
29
    protected $endpoint = 'https://api.goironcloud.com';
30
31
    /**
32
     * @var string
33
     */
34
    protected $version = 'latest';
35
36
    /**
37
     * @var string
38
     */
39
    protected $email = null;
40
41
    /**
42
     * @var string
43
     */
44
    protected $password = null;
45
46
    /**
47
     * @var int
48
     */
49
    protected $containerId = null;
50
51
    /**
52
     * IronBox constructor.
53
     *
54
     * @param \GuzzleHttp\Client|null $client
55
     */
56
    public function __construct(Client $client = null)
57
    {
58
        $this->client = $client ?? new Client;
59
60
        $this->endpoint = "{$this->endpoint}/{$this->version}/";
61
    }
62
63
    /**
64
     * @param string $email
65
     *
66
     * @return $this
67
     */
68
    public function setEmail(string $email)
69
    {
70
        $this->email = $email;
71
72
        return $this;
73
    }
74
75
    /**
76
     * @param string $password
77
     *
78
     * @return $this
79
     */
80
    public function setPassword(string $password)
81
    {
82
        $this->password = $password;
83
84
        return $this;
85
    }
86
87
    /**
88
     * @param $containerId
89
     *
90
     * @return $this
91
     */
92
    public function setContainerId($containerId)
93
    {
94
        $this->containerId = $containerId;
95
96
        return $this;
97
    }
98
99
    /**
100
     * Checks if the IronBox API server is responding
101
     *
102
     * @return bool
103
     *
104
     * @throws \GuzzleHttp\Exception\GuzzleException
105
     * @throws \SGP\IronBox\Exceptions\ServiceUnavailable
106
     */
107
    public function ping()
108
    {
109
        $endpoint = $this->endpoint . 'Ping';
110
111
        $request = $this->client->request('GET', $endpoint);
112
113
        if ($request->getStatusCode() !== 200) {
114
            throw new ServiceUnavailable('IronBox API server is not accessible from this network location');
115
        }
116
117
        return true;
118
    }
119
120
    /**
121
     * Uploads a given file to an IronBox container
122
     *
123
     * @param string $filePath Local file path of file to upload
124
     * @param string $blobName Name of the file to use on cloud storage
125
     *
126
     * @throws \GuzzleHttp\Exception\GuzzleException
127
     * @throws \SGP\IronBox\Exceptions\FileNotFound
128
     * @throws \SGP\IronBox\Exceptions\IronBoxException
129
     * @throws \SGP\IronBox\Exceptions\ServiceUnavailable
130
     */
131
    public function uploadFileToContainer(string $filePath, string $blobName)
132
    {
133
        $this->isFile($filePath);
134
135
        $encryptedFilePath = $filePath . '.iron';
136
137
        $this->ping();
138
139
        $keyData = $this->containerKeyData();
140
141
        $blobIdName = $this->createEntityContainerBlob($blobName);
142
143
        $checkoutData = $this->checkOutEntityContainerBlob($blobIdName);
144
145
        (new FileEncrypter($filePath, $encryptedFilePath, $keyData))->encrypt();
146
147
        $this->uploadBlob($encryptedFilePath, $checkoutData);
148
149
        $this->checkInEntityContainerBlob($blobIdName, $filePath, $checkoutData);
150
151
        $this->removeFile($encryptedFilePath);
152
    }
153
154
    /**
155
     * Fetches an IronBox container key data
156
     *
157
     * @return \SGP\IronBox\ContainerKeyData
158
     *
159
     * @throws \GuzzleHttp\Exception\GuzzleException
160
     * @throws \SGP\IronBox\Exceptions\IronBoxException
161
     * @throws \SGP\IronBox\Exceptions\ServiceUnavailable
162
     */
163
    public function containerKeyData()
164
    {
165
        $endpoint = $this->endpoint . 'ContainerKeyData';
166
167
        $request = $this->client->request('POST', $endpoint, [
168
            'headers' => $this->httpHeaders,
169
            'form_params' => [
170
                'Entity' => $this->email,
171
                'EntityType' => EntityTypes::EMAIL_ADDRESS,
172
                'EntityPassword' => $this->password,
173
                'ContainerID' => $this->containerId,
174
            ],
175
        ]);
176
177
        if ($request->getStatusCode() !== 200) {
178
            throw new ServiceUnavailable('Unable to retrieve container key data');
179
        }
180
181
        $response = json_decode($request->getBody()->getContents(), true);
182
183
        if (! $response ||
184
            ! is_array($response) ||
185
            ! isset($response['SessionKeyBase64'], $response['SessionIVBase64'], $response['SymmetricKeyStrength'])
186
        ) {
187
            throw new IronBoxException('ContainerKeyData call returned invalid data');
188
        }
189
190
        return (new ContainerKeyData)
191
            ->symmetricKey(base64_decode($response['SessionKeyBase64'], true))
192
            ->initializationVector(base64_decode($response['SessionIVBase64'], true))
193
            ->keyStrength($response['SymmetricKeyStrength']);
194
    }
195
196
    /**
197
     * Creates an IronBox blob in an existing container
198
     *
199
     * @param string $blobName
200
     *
201
     * @return string
202
     *
203
     * @throws \GuzzleHttp\Exception\GuzzleException
204
     * @throws \SGP\IronBox\Exceptions\IronBoxException
205
     * @throws \SGP\IronBox\Exceptions\ServiceUnavailable
206
     */
207
    public function createEntityContainerBlob(string $blobName)
208
    {
209
        $endpoint = $this->endpoint . 'CreateEntityContainerBlob';
210
211
        $request = $this->client->request('POST', $endpoint, [
212
            'headers' => $this->httpHeaders,
213
            'form_params' => [
214
                'Entity' => $this->email,
215
                'EntityType' => EntityTypes::EMAIL_ADDRESS,
216
                'EntityPassword' => $this->password,
217
                'ContainerID' => $this->containerId,
218
                'BlobName' => $blobName,
219
            ],
220
        ]);
221
222
        if ($request->getStatusCode() !== 200) {
223
            throw new ServiceUnavailable('Unable to create entity container blob');
224
        }
225
226
        $response = json_decode($request->getBody()->getContents(), true);
227
228
        if (! $response || ! is_string($response)) {
229
            throw new IronBoxException('CreateEntityContainerBlob call returned invalid data');
230
        }
231
232
        return $response;
233
    }
234
235
    /**
236
     * Checks outs an entity container blob, so that the caller can begin uploading the contents of the blob.
237
     *
238
     * @param string $blobIdName
239
     *
240
     * @return \SGP\IronBox\CheckOutData
241
     *
242
     * @throws \GuzzleHttp\Exception\GuzzleException
243
     * @throws \SGP\IronBox\Exceptions\IronBoxException
244
     * @throws \SGP\IronBox\Exceptions\ServiceUnavailable
245
     */
246
    public function checkOutEntityContainerBlob(string $blobIdName)
247
    {
248
        $endpoint = $this->endpoint . 'CheckOutEntityContainerBlob';
249
250
        $request = $this->client->request('POST', $endpoint, [
251
            'headers' => $this->httpHeaders,
252
            'form_params' => [
253
                'Entity' => $this->email,
254
                'EntityType' => EntityTypes::EMAIL_ADDRESS,
255
                'EntityPassword' => $this->password,
256
                'ContainerID' => $this->containerId,
257
                'BlobIDName' => $blobIdName,
258
            ],
259
        ]);
260
261
        if ($request->getStatusCode() !== 200) {
262
            throw new ServiceUnavailable('Unable to check out entity container blob');
263
        }
264
265
        $response = json_decode($request->getBody()->getContents(), true);
266
267
        if (! $response || ! is_array($response)) {
268
            throw new IronBoxException('CheckOutEntityContainerBlob call returned invalid data');
269
        }
270
271
        $containerKeyData = new CheckOutData($response);
272
273
        $containerKeyData->validate();
274
275
        return $containerKeyData;
276
    }
277
278
    /**
279
     * Uploads an encrypted file to cloud storage using the shared access signature provided. This function uploads
280
     * blocks in 4 MB blocks with max 50k blocks, meaning that there is a 200 GB max for any file uploaded.
281
     *
282
     * @param string $encryptedFilePath
283
     * @param \SGP\IronBox\CheckOutData $checkOutData
284
     *
285
     * @return bool
286
     *
287
     * @throws \GuzzleHttp\Exception\GuzzleException
288
     * @throws \SGP\IronBox\Exceptions\FileNotFound
289
     * @throws \SGP\IronBox\Exceptions\IronBoxException
290
     */
291
    public function uploadBlob(string $encryptedFilePath, CheckOutData $checkOutData)
292
    {
293
        $this->isFile($encryptedFilePath);
294
        $checkOutData->validate();
295
296
        $blockSizeBytes = 4 * 1024 * 1024;
297
298
        // Open handle to encrypted file and send it in blocks
299
        $sasUriBlockPrefix = $checkOutData->sharedAccessSignatureUri . '&comp=block&blockid=';
0 ignored issues
show
Bug Best Practice introduced by
The property $sharedAccessSignatureUri is declared protected in SGP\IronBox\CheckOutData. Since you implement __get, consider adding a @property or @property-read.
Loading history...
300
        $blockIds = [];
301
302
        $i = 0;
303
        $fh = fopen($encryptedFilePath, 'r');
304
305
        while (! feof($fh)) {
0 ignored issues
show
It seems like $fh can also be of type false; however, parameter $handle of feof() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

305
        while (! feof(/** @scrutinizer ignore-type */ $fh)) {
Loading history...
306
            $buf = fread($fh, $blockSizeBytes);
0 ignored issues
show
It seems like $fh can also be of type false; however, parameter $handle of fread() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

306
            $buf = fread(/** @scrutinizer ignore-type */ $fh, $blockSizeBytes);
Loading history...
307
308
            // block IDs all have to be the same length, which was NOT documented by MSFT
309
            $blockId = 'block' . str_pad($i, 8, 0, STR_PAD_LEFT);
310
311
            // Create a blob block
312
            $request = $this->client->request('PUT', $sasUriBlockPrefix . base64_encode($blockId), [
313
                'headers' => [
314
                    'content-type' => 'application/octet-stream',
315
                    'x-ms-blob-type' => 'BlockBlob',
316
                    'x-ms-version' => '2012-02-12',
317
                ],
318
                'body' => $buf,
319
            ]);
320
321
            if ($request->getStatusCode() !== 201) {
322
                throw new IronBoxException('Unable to upload file block');
323
            }
324
325
            // Block was successfuly sent, record its ID
326
            $blockIds[] = $blockId;
327
328
            $i += 1;
329
        }
330
331
        // Done sending blocks, so commit the blocks into a single one
332
        // Do the final re-assembly on the storage server side
333
334
        // build list of block ids as xml elements
335
        $blockListBody = '';
336
337
        foreach ($blockIds as $blockId) {
338
            $encodedBlockId = trim(base64_encode($blockId));
339
340
            // Indicate blocks to commit per 2012-02-12 version PUT block list specifications
341
            $blockListBody .= sprintf('<Latest>%s</Latest>', $encodedBlockId);
342
        }
343
344
        $request = $this->client->request('PUT', $checkOutData->sharedAccessSignatureUri . '&comp=blockList', [
345
            'headers' => [
346
                'content-type' => 'text/xml',
347
                'x-ms-version' => '2012-02-12',
348
            ],
349
            'body' => sprintf('<?xml version="1.0" encoding="utf-8"?><BlockList>%s</BlockList>', $blockListBody),
350
        ]);
351
352
        if ($request->getStatusCode() !== 201) {
353
            throw new IronBoxException('Unable to upload blob');
354
        }
355
356
        return true;
357
    }
358
359
    /**
360
     * This method checks-in a checked-out blob indicating to IronBox that this blob is ready.
361
     * This method should only be called after the caller has finished modifying the checked-out blob.
362
     *
363
     * @param string $blobIdName
364
     * @param string $filePath
365
     * @param \SGP\IronBox\CheckOutData $checkoutData
366
     *
367
     * @return bool
368
     *
369
     * @throws \GuzzleHttp\Exception\GuzzleException
370
     * @throws \SGP\IronBox\Exceptions\FileNotFound
371
     * @throws \SGP\IronBox\Exceptions\IronBoxException
372
     * @throws \SGP\IronBox\Exceptions\ServiceUnavailable
373
     */
374
    public function checkInEntityContainerBlob(string $blobIdName, string $filePath, CheckOutData $checkoutData)
375
    {
376
        $this->isFile($filePath);
377
        $checkoutData->validate();
378
379
        $endpoint = $this->endpoint . 'CheckInEntityContainerBlob';
380
381
        $request = $this->client->request('POST', $endpoint, [
382
            'headers' => $this->httpHeaders,
383
            'form_params' => [
384
                'Entity' => $this->email,
385
                'EntityType' => EntityTypes::EMAIL_ADDRESS,
386
                'EntityPassword' => $this->password,
387
                'ContainerID' => $this->containerId,
388
                'BlobIDName' => $blobIdName,
389
                'BlobSizeBytes' => filesize($filePath),
390
                'BlobCheckInToken' => $checkoutData->checkInToken,
0 ignored issues
show
Bug Best Practice introduced by
The property $checkInToken is declared protected in SGP\IronBox\CheckOutData. Since you implement __get, consider adding a @property or @property-read.
Loading history...
391
            ],
392
        ]);
393
394
        if ($request->getStatusCode() !== 200) {
395
            throw new ServiceUnavailable('Unable to check in entity container blob');
396
        }
397
398
        $response = json_decode($request->getBody()->getContents(), true);
399
400
        if ($response !== true) {
401
            throw new IronBoxException('CheckInEntityContainerBlob call returned invalid data');
402
        }
403
404
        return true;
405
    }
406
407
    /**
408
     * Determine if the given path is a file.
409
     *
410
     * @param string $path
411
     *
412
     * @return bool
413
     *
414
     * @throws \SGP\IronBox\Exceptions\FileNotFound
415
     */
416
    private function isFile(string $path)
417
    {
418
        if (is_file($path)) {
419
            return true;
420
        }
421
422
        throw new FileNotFound("File does not exist at path {$path}");
423
    }
424
425
    /**
426
     * @param string $path
427
     *
428
     * @throws \SGP\IronBox\Exceptions\FileNotFound
429
     */
430
    private function removeFile(string $path)
431
    {
432
        $this->isFile($path);
433
434
        unlink($path);
435
    }
436
}
437