Skip to content

Commit 995b567

Browse files
committed
Reimplement encrypted storage
using modern standards via paragonie/halite and libsodium.
1 parent b36d34a commit 995b567

12 files changed

+531
-169
lines changed

README.md

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
# ObjectStorage 2.0 library
1+
# ObjectStorage 3.0 library
22

33
ObjectStorage library for your cloud-based applications.
44

5-
*NOTE: version 1.0, previously only available as dev-master, is still available by updating your composer.json to require version ~1.0*
6-
75
## Object Storage vs a normal file system
86

97
Object-based storage solves large scale storage problems for cloud-based applications.
@@ -78,42 +76,38 @@ $service->delete('my-message');
7876

7977
### Encryption
8078

81-
The library includes an EncryptionAdapter that will allow you to transparently encrypt/decrypt
79+
The library includes adapters to allow you to transparently encrypt/decrypt
8280
your data before it's passed to the storage backend.
8381

84-
This is done by wrapping the original storage adapter (s3, file, pdo, gridfs, etc) into
85-
the EncryptionAdapter. Here's an example
82+
This is done by wrapping the original storage adapter (s3, file, pdo, gridfs,
83+
etc) into the one of the encryption adapters. Here's an example
8684

8785
```php
88-
$adapter = new ObjectStorage\Adapter\PdoAdapter($pdo);
89-
$adapter = new ObjectStorage\Adapter\EncryptionAdapter($adapter, $key, $iv);
90-
// You can use $adapter as before, but all data will be encrypted
86+
$adapter = new \ObjectStorage\Adapter\EncryptedStorageAdapter(
87+
new \ObjectStorage\Adapter\PdoAdapter($pdo),
88+
\ParagonIE\Halite\KeyFactory::loadEncryptionKey($pathToKeyfile)
89+
);
90+
// You can use $adapter as before and both the storage keys and objects will be
91+
// encrypted (use PlaintextKeyEncryptedStorageAdapter if you don't want the
92+
// storage keys to be encrypted).
9193
```
9294

93-
The key and iv are hex encoded strings. To generate these, use the following command:
94-
95-
./bin/objectstorage objectstorage:generatekey
95+
The encryption routines are provided by [ParagonIE/Halite][] and libsodium.
9696

97-
This will output something like the following:
98-
99-
KEY: C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0
100-
IV: E5F3E442F3CE0ECC931B7E866A5F3121
101-
102-
Save these 2 values somewhere safely.
97+
Use the following command to generate an encryption key and save it to a file :-
10398

104-
The encryption is similar to using the following commands:
105-
106-
openssl enc -aes-256-cbc -K C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0 -iv E5F3E442F3CE0ECC931B7E866A5F3121 < original.txt > encrypted.aes
99+
```sh
100+
./bin/objectstorage genkey /path/to/a/file
101+
```
107102

108-
openssl enc -d -aes-256-cbc -K C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0 -iv E5F3E442F3CE0ECC931B7E866A5F3121 < encrypted.aes
109-
110-
You can also use the included encrypt + decrypt commands:
103+
You can also use the included encrypt + decrypt commands. In the following
104+
example we encrypt `example.pdf` with the encryption key in `key.asc` and then
105+
decrypt it again, using the same key and writing it to a new `example-new.pdf`:
111106

112-
export OBJECTSTORAGE_ENCRYPTION_KEY=C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0
113-
export OBJECTSTORAGE_ENCRYPTION_IV=E5F3E442F3CE0ECC931B7E866A5F3121
114-
115-
bin/objectstorage objectstorage:encrypt example.pdf > example.pdf.encrypted
116-
bin/objectstorage objectstorage:decrypt example.pdf.encrypted > example_new.pdf
107+
```sh
108+
bin/objectstorage encrypt key.asc example.pdf example.pdf.encrypted
109+
bin/objectstorage decrypt key.asc example.pdf.encrypted example-new.pdf
110+
```
117111

118112
## Console tool
119113

@@ -172,11 +166,20 @@ Then, add `linkorb/objectstorage` to your project's `composer.json`:
172166
```json
173167
{
174168
"require": {
175-
"linkorb/objectstorage": "~2.0"
169+
"linkorb/objectstorage": "^3.0"
176170
}
177171
}
178172
```
179173

174+
## Older versions of this library
175+
176+
Version 1.0, previously only available as dev-master, is still available by
177+
updating your composer.json to require version "~1.0".
178+
179+
The `php5` branch will still work with PHP <= 5.6, but it will not have the
180+
latest features and, particularly, should not be used if you need encrypted
181+
storage.
182+
180183
## Contributing
181184

182185
Ready to build and improve on this repo? Excellent!
@@ -195,3 +198,5 @@ Btw, we're hiring!
195198
## License
196199

197200
Please check LICENSE.md for full license information
201+
202+
[ParagonIE/Halite]: <https://paragonie.com/project/halite> "Halite - Simple PHP Cryptography Library"

bin/objectstorage

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ use ObjectStorage\Command\UploadCommand;
1313
use Symfony\Component\Console\Application;
1414
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;
1515

16-
$application = new Application('ObjectStorage CLI utility', '1.0.0');
16+
$application = new Application('ObjectStorage CLI utility', 'v3');
1717
$application->setCatchExceptions(true);
1818
$application->setCommandLoader(new FactoryCommandLoader([
1919
'objectstorage:upload' => function () { return new UploadCommand(); },
2020
'objectstorage:download' => function () { return new DownloadCommand(); },
2121
'objectstorage:list' => function () { return new ListCommand(); },
2222
'objectstorage:delete' => function () { return new DeleteCommand(); },
23-
'objectstorage:generatekey' => function () { return new GenerateKeyCommand(); },
24-
'objectstorage:encrypt' => function () { return new EncryptCommand(); },
25-
'objectstorage:decrypt' => function () { return new DecryptCommand(); },
23+
'genkey' => function () { return new GenerateKeyCommand(); },
24+
'encrypt' => function () { return new EncryptCommand(); },
25+
'decrypt' => function () { return new DecryptCommand(); },
2626
]));
2727
$application->run();

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
}
1313
],
1414
"require": {
15-
"php": "^7.2"
15+
"php": "^7.2",
16+
"paragonie/halite": "^4"
1617
},
1718
"require-dev": {
1819
"symfony/console": "^4",
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
namespace ObjectStorage\Adapter;
4+
5+
use ParagonIE\Halite\Alerts\CannotPerformOperation;
6+
use ParagonIE\Halite\Halite;
7+
use ParagonIE\Halite\KeyFactory;
8+
use ParagonIE\Halite\Symmetric\Crypto;
9+
use ParagonIE\Halite\Symmetric\EncryptionKey;
10+
use ParagonIE\HiddenString\HiddenString;
11+
12+
/**
13+
* Decorates a storage adapter to encrypt and decrypt object data and the keys
14+
* by which the data are stored.
15+
*/
16+
class EncryptedStorageAdapter implements StorageAdapterInterface
17+
{
18+
const CFG_ENCRYPTION_KEY = 'encryption_key';
19+
const CFG_ENCRYPTION_KEY_PATH = 'encryption_key_path';
20+
const CFG_STORAGE_ADAPTER = 'storage_adapter';
21+
22+
protected $encryptionKey;
23+
protected $storageAdapter;
24+
25+
public static function build(array $config)
26+
{
27+
if (!isset($config[self::CFG_ENCRYPT_STORAGE_ADAPTER])
28+
|| !$config[self::CFG_ENCRYPT_STORAGE_ADAPTER] instanceof StorageAdapterInterface
29+
) {
30+
throw new \InvalidArgumentException(
31+
'The build configuration for this storage adapter is missing an instance of StorageAdapterInterface, keyed as "'
32+
. self::CFG_STORAGE_ADAPTER
33+
. '"."'
34+
);
35+
}
36+
37+
if (isset($config[self::CFG_ENCRYPTION_KEY])) {
38+
if (!$config[self::CFG_ENCRYPTION_KEY] instanceof EncryptionKey) {
39+
throw new \InvalidArgumentException(
40+
'"' . self::CFG_ENCRYPTION_KEY . '" must be an instance of EncryptionKey.'
41+
);
42+
}
43+
$encryptionKey = $config[self::CFG_ENCRYPTION_KEY];
44+
} elseif (isset($config[self::CFG_ENCRYPTION_KEY_PATH])) {
45+
try {
46+
$encryptionKey = KeyFactory::loadEncryptionKey($config[self::CFG_ENCRYPTION_KEY_PATH]);
47+
} catch (CannotPerformOperation $e) {
48+
throw new \InvalidArgumentException(
49+
'"' . self::CFG_ENCRYPTION_KEY_PATH . '" must be a readable file.'
50+
);
51+
}
52+
} else {
53+
throw new \InvalidArgumentException(
54+
'The build configuration for this storage adapter is missing an encryption key ("'
55+
. self::CFG_ENCRYPTION_KEY
56+
. '" or "'
57+
. self::CFG_ENCRYPTION_KEY_PATH
58+
. '").'
59+
);
60+
}
61+
62+
return new self(
63+
$config[self::CFG_ENCRYPT_STORAGE_ADAPTER],
64+
$encryptionKey
65+
);
66+
}
67+
68+
public function __construct(
69+
StorageAdapterInterface $storageAdapter,
70+
EncryptionKey $encryptionKey
71+
) {
72+
$this->storageAdapter = $storageAdapter;
73+
$this->encryptionKey = $encryptionKey;
74+
}
75+
76+
public function setAdapter(StorageAdapterInterface $storageAdapter)
77+
{
78+
$this->storageAdapter = $storageAdapter;
79+
}
80+
81+
public function setEncryptionKey(EncryptionKey $encryptionKey)
82+
{
83+
$this->encryptionKey = $encryptionKey;
84+
}
85+
86+
public function setData($key, $data)
87+
{
88+
try {
89+
$encryptedStorageKey = Crypto::encrypt(
90+
new HiddenString($key),
91+
$this->encryptionKey,
92+
Halite::ENCODE_BASE64URLSAFE
93+
);
94+
} catch (CannotPerformOperation $e) {
95+
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
96+
}
97+
98+
try {
99+
$encryptedData = Crypto::encrypt(
100+
new HiddenString($data),
101+
$this->encryptionKey,
102+
Halite::ENCODE_BASE64URLSAFE
103+
);
104+
} catch (CannotPerformOperation $e) {
105+
throw new EncryptionFailureException('Failed to encrypt the object data.', null, $e);
106+
}
107+
108+
return $this->storageAdapter->setData($encryptedStorageKey, $encryptedData);
109+
}
110+
111+
public function getData($key)
112+
{
113+
try {
114+
$encryptedStorageKey = Crypto::encrypt(
115+
new HiddenString($key),
116+
$this->encryptionKey,
117+
Halite::ENCODE_BASE64URLSAFE
118+
);
119+
} catch (CannotPerformOperation $e) {
120+
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
121+
}
122+
123+
$encryptedData = $this->storageAdapter->getData($encryptedStorageKey);
124+
125+
try {
126+
$plaintextData = (string) Crypto::decrypt($encryptedData, $this->encryptionKey);
127+
} catch (CannotPerformOperation $e) {
128+
throw new EncryptionFailureException('Failed to decrypt the object data.', null, $e);
129+
}
130+
131+
return $plaintextData;
132+
}
133+
134+
public function deleteData($key)
135+
{
136+
try {
137+
$encryptedStorageKey = Crypto::encrypt(
138+
new HiddenString($key),
139+
$this->encryptionKey,
140+
Halite::ENCODE_BASE64URLSAFE
141+
);
142+
} catch (CannotPerformOperation $e) {
143+
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
144+
}
145+
146+
return $this->storageAdapter->deleteData($encryptedStorageKey);
147+
}
148+
}

src/Adapter/EncryptionAdapter.php

Lines changed: 0 additions & 56 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ObjectStorage\Adapter;
4+
5+
class EncryptionFailureException extends \Exception
6+
{
7+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace ObjectStorage\Adapter;
4+
5+
use ParagonIE\Halite\Alerts\CannotPerformOperation;
6+
use ParagonIE\Halite\Halite;
7+
use ParagonIE\Halite\Symmetric\Crypto;
8+
use ParagonIE\HiddenString\HiddenString;
9+
10+
/**
11+
* Decorates a storage adapter to encrypt and decrypt the object data.
12+
*
13+
* Does not encrypt the keys by which data are stored.
14+
*/
15+
class PlaintextStorageKeyEncryptedStorageAdapter extends EncryptedStorageAdapter
16+
{
17+
public function setData($key, $data)
18+
{
19+
try {
20+
$encryptedData = Crypto::encrypt(
21+
new HiddenString($data),
22+
$this->encryptionKey,
23+
Halite::ENCODE_BASE64URLSAFE
24+
);
25+
} catch (CannotPerformOperation $e) {
26+
throw new EncryptionFailureException('Failed to encrypt the object data.', null, $e);
27+
}
28+
29+
return $this->storageAdapter->setData($key, $encryptedData);
30+
}
31+
32+
public function getData($key)
33+
{
34+
$encryptedData = $this->storageAdapter->getData($key);
35+
36+
try {
37+
$plaintextData = (string) Crypto::decrypt($encryptedData, $this->encryptionKey);
38+
} catch (CannotPerformOperation $e) {
39+
throw new EncryptionFailureException('Failed to decrypt the object data.', null, $e);
40+
}
41+
42+
return $plaintextData;
43+
}
44+
45+
public function deleteData($key)
46+
{
47+
return $this->storageAdapter->deleteData($key);
48+
}
49+
}

0 commit comments

Comments
 (0)