Skip to content

Commit 6004442

Browse files
authored
Merge pull request #5 from linkorb/develop
Fix broken storage key encryption; S3 adapter fixes
2 parents b26fee5 + ed81287 commit 6004442

File tree

9 files changed

+244
-73
lines changed

9 files changed

+244
-73
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ etc) into the one of the encryption adapters. Here's an example
8585
```php
8686
$adapter = new \ObjectStorage\Adapter\EncryptedStorageAdapter(
8787
new \ObjectStorage\Adapter\PdoAdapter($pdo),
88-
\ParagonIE\Halite\KeyFactory::loadEncryptionKey($pathToKeyfile)
88+
\ParagonIE\Halite\KeyFactory::loadAuthenticationKey($pathToSigningKey),
89+
\ParagonIE\Halite\KeyFactory::loadEncryptionKey($pathToEncryptionKey)
8990
);
9091
// You can use $adapter as before and both the storage keys and objects will be
9192
// encrypted (use PlaintextKeyEncryptedStorageAdapter if you don't want the
@@ -94,10 +95,12 @@ $adapter = new \ObjectStorage\Adapter\EncryptedStorageAdapter(
9495

9596
The encryption routines are provided by [ParagonIE/Halite][] and libsodium.
9697

97-
Use the following command to generate an encryption key and save it to a file :-
98+
Use the following commands to generate and save a signing key and an encryption
99+
key as needed in the previous example:-
98100

99101
```sh
100-
./bin/objectstorage genkey /path/to/a/file
102+
./bin/objectstorage genkey --signing /path/to/a/file
103+
./bin/objectstorage genkey /path/to/another/file
101104
```
102105

103106
You can also use the included encrypt + decrypt commands. In the following

composer.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"symfony/console": "^4",
2020
"aws/aws-sdk-php": "^3",
2121
"friendsofphp/php-cs-fixer": "^2.15",
22+
"phpstan/phpstan": "^0.11.8",
2223
"phpunit/phpunit": "^8.1"
2324
},
2425
"suggest": {
@@ -29,5 +30,10 @@
2930
"ObjectStorage\\": "src/"
3031
}
3132
},
33+
"autoload-dev": {
34+
"psr-4": {
35+
"ObjectStorage\\Test\\": "tests/"
36+
}
37+
},
3238
"license": "MIT"
3339
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace ObjectStorage\Adapter;
4+
5+
use ParagonIE\Halite\Symmetric\EncryptionKey;
6+
7+
/**
8+
* Base class for encrypted storage adapters.
9+
*/
10+
abstract class AbstractEncryptedStorageAdapter
11+
{
12+
const CFG_AUTHENTICATION_KEY = 'authentication_key';
13+
const CFG_AUTHENTICATION_KEY_PATH = 'authentication_key_path';
14+
const CFG_ENCRYPTION_KEY = 'encryption_key';
15+
const CFG_ENCRYPTION_KEY_PATH = 'encryption_key_path';
16+
const CFG_STORAGE_ADAPTER = 'storage_adapter';
17+
18+
protected $encryptionKey;
19+
protected $storageAdapter;
20+
21+
public function setAdapter(StorageAdapterInterface $storageAdapter)
22+
{
23+
$this->storageAdapter = $storageAdapter;
24+
}
25+
26+
public function setEncryptionKey(EncryptionKey $encryptionKey)
27+
{
28+
$this->encryptionKey = $encryptionKey;
29+
}
30+
}

src/Adapter/EncryptedStorageAdapter.php

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use ParagonIE\Halite\Alerts\CannotPerformOperation;
66
use ParagonIE\Halite\Halite;
77
use ParagonIE\Halite\KeyFactory;
8+
use ParagonIE\Halite\Symmetric\AuthenticationKey;
89
use ParagonIE\Halite\Symmetric\Crypto;
910
use ParagonIE\Halite\Symmetric\EncryptionKey;
1011
use ParagonIE\HiddenString\HiddenString;
@@ -13,19 +14,16 @@
1314
* Decorates a storage adapter to encrypt and decrypt object data and the keys
1415
* by which the data are stored.
1516
*/
16-
class EncryptedStorageAdapter implements StorageAdapterInterface
17+
class EncryptedStorageAdapter extends AbstractEncryptedStorageAdapter implements StorageAdapterInterface
1718
{
18-
const CFG_ENCRYPTION_KEY = 'encryption_key';
19-
const CFG_ENCRYPTION_KEY_PATH = 'encryption_key_path';
20-
const CFG_STORAGE_ADAPTER = 'storage_adapter';
21-
19+
protected $authenticationKey;
2220
protected $encryptionKey;
2321
protected $storageAdapter;
2422

2523
public static function build(array $config)
2624
{
27-
if (!isset($config[self::CFG_ENCRYPT_STORAGE_ADAPTER])
28-
|| !$config[self::CFG_ENCRYPT_STORAGE_ADAPTER] instanceof StorageAdapterInterface
25+
if (!isset($config[self::CFG_STORAGE_ADAPTER])
26+
|| !$config[self::CFG_STORAGE_ADAPTER] instanceof StorageAdapterInterface
2927
) {
3028
throw new \InvalidArgumentException(
3129
'The build configuration for this storage adapter is missing an instance of StorageAdapterInterface, keyed as "'
@@ -34,6 +32,31 @@ public static function build(array $config)
3432
);
3533
}
3634

35+
if (isset($config[self::CFG_AUTHENTICATION_KEY])) {
36+
if (!$config[self::CFG_AUTHENTICATION_KEY] instanceof AuthenticationKey) {
37+
throw new \InvalidArgumentException(
38+
'"' . self::CFG_AUTHENTICATION_KEY . '" must be an instance of AuthenticationKey.'
39+
);
40+
}
41+
$authenticationKey = $config[self::CFG_AUTHENTICATION_KEY];
42+
} elseif (isset($config[self::CFG_AUTHENTICATION_KEY_PATH])) {
43+
try {
44+
$authenticationKey = KeyFactory::loadAuthenticationKey($config[self::CFG_AUTHENTICATION_KEY_PATH]);
45+
} catch (CannotPerformOperation $e) {
46+
throw new \InvalidArgumentException(
47+
'"' . self::CFG_AUTHENTICATION_KEY_PATH . '" must be a readable file.'
48+
);
49+
}
50+
} else {
51+
throw new \InvalidArgumentException(
52+
'The build configuration for this storage adapter is missing an authentication key ("'
53+
. self::CFG_AUTHENTICATION_KEY
54+
. '" or "'
55+
. self::CFG_AUTHENTICATION_KEY_PATH
56+
. '").'
57+
);
58+
}
59+
3760
if (isset($config[self::CFG_ENCRYPTION_KEY])) {
3861
if (!$config[self::CFG_ENCRYPTION_KEY] instanceof EncryptionKey) {
3962
throw new \InvalidArgumentException(
@@ -60,89 +83,97 @@ public static function build(array $config)
6083
}
6184

6285
return new self(
63-
$config[self::CFG_ENCRYPT_STORAGE_ADAPTER],
86+
$config[self::CFG_STORAGE_ADAPTER],
87+
$authenticationKey,
6488
$encryptionKey
6589
);
6690
}
6791

6892
public function __construct(
6993
StorageAdapterInterface $storageAdapter,
94+
AuthenticationKey $authenticationKey,
7095
EncryptionKey $encryptionKey
7196
) {
7297
$this->storageAdapter = $storageAdapter;
98+
$this->authenticationKey = $authenticationKey;
7399
$this->encryptionKey = $encryptionKey;
74100
}
75101

76-
public function setAdapter(StorageAdapterInterface $storageAdapter)
102+
public function setAuthenticationKey(AuthenticationKey $authenticationKey)
77103
{
78-
$this->storageAdapter = $storageAdapter;
79-
}
80-
81-
public function setEncryptionKey(EncryptionKey $encryptionKey)
82-
{
83-
$this->encryptionKey = $encryptionKey;
104+
$this->authenticationKey = $authenticationKey;
84105
}
85106

86107
public function setData($key, $data)
87108
{
88109
try {
89-
$encryptedStorageKey = Crypto::encrypt(
90-
new HiddenString($key),
91-
$this->encryptionKey,
110+
$authenticStorageKey = Crypto::authenticate(
111+
$key,
112+
$this->authenticationKey,
92113
Halite::ENCODE_BASE64URLSAFE
93114
);
94115
} catch (CannotPerformOperation $e) {
95-
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
116+
throw new EncryptionFailureException('Failed to hash the storage key.', null, $e);
96117
}
97118

98119
try {
99-
$encryptedData = Crypto::encrypt(
100-
new HiddenString($data),
120+
$encryptedBlob = Crypto::encrypt(
121+
new HiddenString($key . $data),
101122
$this->encryptionKey,
102123
Halite::ENCODE_BASE64URLSAFE
103124
);
104125
} catch (CannotPerformOperation $e) {
105-
throw new EncryptionFailureException('Failed to encrypt the object data.', null, $e);
126+
throw new EncryptionFailureException('Failed to encrypt the storage key and object data.', null, $e);
106127
}
107128

108-
return $this->storageAdapter->setData($encryptedStorageKey, $encryptedData);
129+
return $this->storageAdapter->setData($authenticStorageKey, $encryptedBlob);
109130
}
110131

111132
public function getData($key)
112133
{
113134
try {
114-
$encryptedStorageKey = Crypto::encrypt(
115-
new HiddenString($key),
116-
$this->encryptionKey,
135+
$authenticStorageKey = Crypto::authenticate(
136+
$key,
137+
$this->authenticationKey,
117138
Halite::ENCODE_BASE64URLSAFE
118139
);
119140
} catch (CannotPerformOperation $e) {
120-
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
141+
throw new EncryptionFailureException('Failed to hash the storage key.', null, $e);
121142
}
122143

123-
$encryptedData = $this->storageAdapter->getData($encryptedStorageKey);
144+
$encryptedBlob = $this->storageAdapter->getData($authenticStorageKey);
124145

125146
try {
126-
$plaintextData = (string) Crypto::decrypt($encryptedData, $this->encryptionKey);
147+
$plaintextBlob = (string) Crypto::decrypt($encryptedBlob, $this->encryptionKey);
127148
} catch (CannotPerformOperation $e) {
128-
throw new EncryptionFailureException('Failed to decrypt the object data.', null, $e);
149+
throw new EncryptionFailureException('Failed to decrypt the storage key and object data.', null, $e);
129150
}
130151

131-
return $plaintextData;
152+
$storageKeyLength = \strlen($key);
153+
154+
if (false === \hash_equals($key, \substr($plaintextBlob, 0, $storageKeyLength))) {
155+
// The $plaintextBlob was definitely encrypted with our encryption key,
156+
// but the storage key is not the one in that blob.
157+
throw new EncryptionFailureException(
158+
'The object data is not the expected one for the supplied storage key. The store has been corrupted or tampered with.'
159+
);
160+
}
161+
162+
return \substr($plaintextBlob, $storageKeyLength);
132163
}
133164

134165
public function deleteData($key)
135166
{
136167
try {
137-
$encryptedStorageKey = Crypto::encrypt(
138-
new HiddenString($key),
139-
$this->encryptionKey,
168+
$authenticStorageKey = Crypto::authenticate(
169+
$key,
170+
$this->authenticationKey,
140171
Halite::ENCODE_BASE64URLSAFE
141172
);
142173
} catch (CannotPerformOperation $e) {
143-
throw new EncryptionFailureException('Failed to encrypt the storage key.', null, $e);
174+
throw new EncryptionFailureException('Failed to hash the storage key.', null, $e);
144175
}
145176

146-
return $this->storageAdapter->deleteData($encryptedStorageKey);
177+
return $this->storageAdapter->deleteData($authenticStorageKey);
147178
}
148179
}

src/Adapter/PlaintextStorageKeyEncryptedStorageAdapter.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,69 @@
44

55
use ParagonIE\Halite\Alerts\CannotPerformOperation;
66
use ParagonIE\Halite\Halite;
7+
use ParagonIE\Halite\KeyFactory;
78
use ParagonIE\Halite\Symmetric\Crypto;
9+
use ParagonIE\Halite\Symmetric\EncryptionKey;
810
use ParagonIE\HiddenString\HiddenString;
911

1012
/**
1113
* Decorates a storage adapter to encrypt and decrypt the object data.
1214
*
1315
* Does not encrypt the keys by which data are stored.
1416
*/
15-
class PlaintextStorageKeyEncryptedStorageAdapter extends EncryptedStorageAdapter
17+
class PlaintextStorageKeyEncryptedStorageAdapter extends AbstractEncryptedStorageAdapter implements StorageAdapterInterface
1618
{
19+
public static function build(array $config)
20+
{
21+
if (!isset($config[self::CFG_STORAGE_ADAPTER])
22+
|| !$config[self::CFG_STORAGE_ADAPTER] instanceof StorageAdapterInterface
23+
) {
24+
throw new \InvalidArgumentException(
25+
'The build configuration for this storage adapter is missing an instance of StorageAdapterInterface, keyed as "'
26+
. self::CFG_STORAGE_ADAPTER
27+
. '"."'
28+
);
29+
}
30+
31+
if (isset($config[self::CFG_ENCRYPTION_KEY])) {
32+
if (!$config[self::CFG_ENCRYPTION_KEY] instanceof EncryptionKey) {
33+
throw new \InvalidArgumentException(
34+
'"' . self::CFG_ENCRYPTION_KEY . '" must be an instance of EncryptionKey.'
35+
);
36+
}
37+
$encryptionKey = $config[self::CFG_ENCRYPTION_KEY];
38+
} elseif (isset($config[self::CFG_ENCRYPTION_KEY_PATH])) {
39+
try {
40+
$encryptionKey = KeyFactory::loadEncryptionKey($config[self::CFG_ENCRYPTION_KEY_PATH]);
41+
} catch (CannotPerformOperation $e) {
42+
throw new \InvalidArgumentException(
43+
'"' . self::CFG_ENCRYPTION_KEY_PATH . '" must be a readable file.'
44+
);
45+
}
46+
} else {
47+
throw new \InvalidArgumentException(
48+
'The build configuration for this storage adapter is missing an encryption key ("'
49+
. self::CFG_ENCRYPTION_KEY
50+
. '" or "'
51+
. self::CFG_ENCRYPTION_KEY_PATH
52+
. '").'
53+
);
54+
}
55+
56+
return new self(
57+
$config[self::CFG_STORAGE_ADAPTER],
58+
$encryptionKey
59+
);
60+
}
61+
62+
public function __construct(
63+
StorageAdapterInterface $storageAdapter,
64+
EncryptionKey $encryptionKey
65+
) {
66+
$this->storageAdapter = $storageAdapter;
67+
$this->encryptionKey = $encryptionKey;
68+
}
69+
1770
public function setData($key, $data)
1871
{
1972
try {

0 commit comments

Comments
 (0)