Skip to content

Commit

Permalink
Reimplement encrypted storage
Browse files Browse the repository at this point in the history
using modern standards via paragonie/halite and libsodium.
  • Loading branch information
boite committed May 31, 2019
1 parent b36d34a commit 995b567
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 169 deletions.
65 changes: 35 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# ObjectStorage 2.0 library
# ObjectStorage 3.0 library

ObjectStorage library for your cloud-based applications.

*NOTE: version 1.0, previously only available as dev-master, is still available by updating your composer.json to require version ~1.0*

## Object Storage vs a normal file system

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

### Encryption

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

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

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

The key and iv are hex encoded strings. To generate these, use the following command:

./bin/objectstorage objectstorage:generatekey
The encryption routines are provided by [ParagonIE/Halite][] and libsodium.

This will output something like the following:

KEY: C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0
IV: E5F3E442F3CE0ECC931B7E866A5F3121

Save these 2 values somewhere safely.
Use the following command to generate an encryption key and save it to a file :-

The encryption is similar to using the following commands:

openssl enc -aes-256-cbc -K C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0 -iv E5F3E442F3CE0ECC931B7E866A5F3121 < original.txt > encrypted.aes
```sh
./bin/objectstorage genkey /path/to/a/file
```

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

export OBJECTSTORAGE_ENCRYPTION_KEY=C2FE680A5613469189621C9E46B52C15C9C80E50370E7950D6FD2D027C4FAEF0
export OBJECTSTORAGE_ENCRYPTION_IV=E5F3E442F3CE0ECC931B7E866A5F3121

bin/objectstorage objectstorage:encrypt example.pdf > example.pdf.encrypted
bin/objectstorage objectstorage:decrypt example.pdf.encrypted > example_new.pdf
```sh
bin/objectstorage encrypt key.asc example.pdf example.pdf.encrypted
bin/objectstorage decrypt key.asc example.pdf.encrypted example-new.pdf
```

## Console tool

Expand Down Expand Up @@ -172,11 +166,20 @@ Then, add `linkorb/objectstorage` to your project's `composer.json`:
```json
{
"require": {
"linkorb/objectstorage": "~2.0"
"linkorb/objectstorage": "^3.0"
}
}
```

## Older versions of this library

Version 1.0, previously only available as dev-master, is still available by
updating your composer.json to require version "~1.0".

The `php5` branch will still work with PHP <= 5.6, but it will not have the
latest features and, particularly, should not be used if you need encrypted
storage.

## Contributing

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

Please check LICENSE.md for full license information

[ParagonIE/Halite]: <https://paragonie.com/project/halite> "Halite - Simple PHP Cryptography Library"
8 changes: 4 additions & 4 deletions bin/objectstorage
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ use ObjectStorage\Command\UploadCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;

$application = new Application('ObjectStorage CLI utility', '1.0.0');
$application = new Application('ObjectStorage CLI utility', 'v3');
$application->setCatchExceptions(true);
$application->setCommandLoader(new FactoryCommandLoader([
'objectstorage:upload' => function () { return new UploadCommand(); },
'objectstorage:download' => function () { return new DownloadCommand(); },
'objectstorage:list' => function () { return new ListCommand(); },
'objectstorage:delete' => function () { return new DeleteCommand(); },
'objectstorage:generatekey' => function () { return new GenerateKeyCommand(); },
'objectstorage:encrypt' => function () { return new EncryptCommand(); },
'objectstorage:decrypt' => function () { return new DecryptCommand(); },
'genkey' => function () { return new GenerateKeyCommand(); },
'encrypt' => function () { return new EncryptCommand(); },
'decrypt' => function () { return new DecryptCommand(); },
]));
$application->run();
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
}
],
"require": {
"php": "^7.2"
"php": "^7.2",
"paragonie/halite": "^4"
},
"require-dev": {
"symfony/console": "^4",
Expand Down
148 changes: 148 additions & 0 deletions src/Adapter/EncryptedStorageAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace ObjectStorage\Adapter;

use ParagonIE\Halite\Alerts\CannotPerformOperation;
use ParagonIE\Halite\Halite;
use ParagonIE\Halite\KeyFactory;
use ParagonIE\Halite\Symmetric\Crypto;
use ParagonIE\Halite\Symmetric\EncryptionKey;
use ParagonIE\HiddenString\HiddenString;

/**
* Decorates a storage adapter to encrypt and decrypt object data and the keys
* by which the data are stored.
*/
class EncryptedStorageAdapter implements StorageAdapterInterface
{
const CFG_ENCRYPTION_KEY = 'encryption_key';
const CFG_ENCRYPTION_KEY_PATH = 'encryption_key_path';
const CFG_STORAGE_ADAPTER = 'storage_adapter';

protected $encryptionKey;
protected $storageAdapter;

public static function build(array $config)
{
if (!isset($config[self::CFG_ENCRYPT_STORAGE_ADAPTER])
|| !$config[self::CFG_ENCRYPT_STORAGE_ADAPTER] instanceof StorageAdapterInterface
) {
throw new \InvalidArgumentException(
'The build configuration for this storage adapter is missing an instance of StorageAdapterInterface, keyed as "'
. self::CFG_STORAGE_ADAPTER
. '"."'
);
}

if (isset($config[self::CFG_ENCRYPTION_KEY])) {
if (!$config[self::CFG_ENCRYPTION_KEY] instanceof EncryptionKey) {
throw new \InvalidArgumentException(
'"' . self::CFG_ENCRYPTION_KEY . '" must be an instance of EncryptionKey.'
);
}
$encryptionKey = $config[self::CFG_ENCRYPTION_KEY];
} elseif (isset($config[self::CFG_ENCRYPTION_KEY_PATH])) {
try {
$encryptionKey = KeyFactory::loadEncryptionKey($config[self::CFG_ENCRYPTION_KEY_PATH]);
} catch (CannotPerformOperation $e) {
throw new \InvalidArgumentException(
'"' . self::CFG_ENCRYPTION_KEY_PATH . '" must be a readable file.'
);
}
} else {
throw new \InvalidArgumentException(
'The build configuration for this storage adapter is missing an encryption key ("'
. self::CFG_ENCRYPTION_KEY
. '" or "'
. self::CFG_ENCRYPTION_KEY_PATH
. '").'
);
}

return new self(
$config[self::CFG_ENCRYPT_STORAGE_ADAPTER],
$encryptionKey
);
}

public function __construct(
StorageAdapterInterface $storageAdapter,
EncryptionKey $encryptionKey
) {
$this->storageAdapter = $storageAdapter;
$this->encryptionKey = $encryptionKey;
}

public function setAdapter(StorageAdapterInterface $storageAdapter)
{
$this->storageAdapter = $storageAdapter;
}

public function setEncryptionKey(EncryptionKey $encryptionKey)
{
$this->encryptionKey = $encryptionKey;
}

public function setData($key, $data)
{
try {
$encryptedStorageKey = Crypto::encrypt(
new HiddenString($key),
$this->encryptionKey,
Halite::ENCODE_BASE64URLSAFE
);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
}

try {
$encryptedData = Crypto::encrypt(
new HiddenString($data),
$this->encryptionKey,
Halite::ENCODE_BASE64URLSAFE
);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to encrypt the object data.', null, $e);
}

return $this->storageAdapter->setData($encryptedStorageKey, $encryptedData);
}

public function getData($key)
{
try {
$encryptedStorageKey = Crypto::encrypt(
new HiddenString($key),
$this->encryptionKey,
Halite::ENCODE_BASE64URLSAFE
);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
}

$encryptedData = $this->storageAdapter->getData($encryptedStorageKey);

try {
$plaintextData = (string) Crypto::decrypt($encryptedData, $this->encryptionKey);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to decrypt the object data.', null, $e);
}

return $plaintextData;
}

public function deleteData($key)
{
try {
$encryptedStorageKey = Crypto::encrypt(
new HiddenString($key),
$this->encryptionKey,
Halite::ENCODE_BASE64URLSAFE
);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
}

return $this->storageAdapter->deleteData($encryptedStorageKey);
}
}
56 changes: 0 additions & 56 deletions src/Adapter/EncryptionAdapter.php

This file was deleted.

7 changes: 7 additions & 0 deletions src/Adapter/EncryptionFailureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace ObjectStorage\Adapter;

class EncryptionFailureException extends \Exception
{
}
49 changes: 49 additions & 0 deletions src/Adapter/PlaintextStorageKeyEncryptedStorageAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace ObjectStorage\Adapter;

use ParagonIE\Halite\Alerts\CannotPerformOperation;
use ParagonIE\Halite\Halite;
use ParagonIE\Halite\Symmetric\Crypto;
use ParagonIE\HiddenString\HiddenString;

/**
* Decorates a storage adapter to encrypt and decrypt the object data.
*
* Does not encrypt the keys by which data are stored.
*/
class PlaintextStorageKeyEncryptedStorageAdapter extends EncryptedStorageAdapter
{
public function setData($key, $data)
{
try {
$encryptedData = Crypto::encrypt(
new HiddenString($data),
$this->encryptionKey,
Halite::ENCODE_BASE64URLSAFE
);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to encrypt the object data.', null, $e);
}

return $this->storageAdapter->setData($key, $encryptedData);
}

public function getData($key)
{
$encryptedData = $this->storageAdapter->getData($key);

try {
$plaintextData = (string) Crypto::decrypt($encryptedData, $this->encryptionKey);
} catch (CannotPerformOperation $e) {
throw new EncryptionFailureException('Failed to decrypt the object data.', null, $e);
}

return $plaintextData;
}

public function deleteData($key)
{
return $this->storageAdapter->deleteData($key);
}
}

0 comments on commit 995b567

Please sign in to comment.