forked from phacility/phabricator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPhabricatorFileAES256StorageFormat.php
215 lines (164 loc) · 6.04 KB
/
PhabricatorFileAES256StorageFormat.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php
/**
* At-rest encryption format using AES256 CBC.
*/
final class PhabricatorFileAES256StorageFormat
extends PhabricatorFileStorageFormat {
const FORMATKEY = 'aes-256-cbc';
private $keyName;
public function getStorageFormatName() {
return pht('Encrypted (AES-256-CBC)');
}
public function canGenerateNewKeyMaterial() {
return true;
}
public function generateNewKeyMaterial() {
$envelope = self::newAES256Key();
$material = $envelope->openEnvelope();
return base64_encode($material);
}
public function canCycleMasterKey() {
return true;
}
public function cycleStorageProperties() {
$file = $this->getFile();
list($key, $iv) = $this->extractKeyAndIV($file);
return $this->formatStorageProperties($key, $iv);
}
public function newReadIterator($raw_iterator) {
$file = $this->getFile();
$data = $file->loadDataFromIterator($raw_iterator);
list($key, $iv) = $this->extractKeyAndIV($file);
$data = $this->decryptData($data, $key, $iv);
return array($data);
}
public function newWriteIterator($raw_iterator) {
$file = $this->getFile();
$data = $file->loadDataFromIterator($raw_iterator);
list($key, $iv) = $this->extractKeyAndIV($file);
$data = $this->encryptData($data, $key, $iv);
return array($data);
}
public function newFormatIntegrityHash() {
$file = $this->getFile();
list($key_envelope, $iv_envelope) = $this->extractKeyAndIV($file);
// NOTE: We include the IV in the format integrity hash. If we do not,
// attackers can potentially forge the first block of decrypted data
// in CBC mode if they are able to substitute a chosen IV and predict
// the plaintext. (Normally, they can not tamper with the IV.)
$input = self::FORMATKEY.'/iv:'.$iv_envelope->openEnvelope();
return PhabricatorHash::digestWithNamedKey(
$input,
PhabricatorFileStorageEngine::HMAC_INTEGRITY);
}
public function newStorageProperties() {
// Generate a unique key and IV for this block of data.
$key_envelope = self::newAES256Key();
$iv_envelope = self::newAES256IV();
return $this->formatStorageProperties($key_envelope, $iv_envelope);
}
private function formatStorageProperties(
PhutilOpaqueEnvelope $key_envelope,
PhutilOpaqueEnvelope $iv_envelope) {
// Encode the raw binary data with base64 so we can wrap it in JSON.
$data = array(
'iv.base64' => base64_encode($iv_envelope->openEnvelope()),
'key.base64' => base64_encode($key_envelope->openEnvelope()),
);
// Encode the base64 data with JSON.
$data_clear = phutil_json_encode($data);
// Encrypt the block key with the master key, using a unique IV.
$data_iv = self::newAES256IV();
$key_name = $this->getMasterKeyName();
$master_key = $this->getMasterKeyMaterial($key_name);
$data_cipher = $this->encryptData($data_clear, $master_key, $data_iv);
return array(
'key.name' => $key_name,
'iv.base64' => base64_encode($data_iv->openEnvelope()),
'payload.base64' => base64_encode($data_cipher),
);
}
private function extractKeyAndIV(PhabricatorFile $file) {
$outer_iv = $file->getStorageProperty('iv.base64');
$outer_iv = base64_decode($outer_iv);
$outer_iv = new PhutilOpaqueEnvelope($outer_iv);
$outer_payload = $file->getStorageProperty('payload.base64');
$outer_payload = base64_decode($outer_payload);
$outer_key_name = $file->getStorageProperty('key.name');
$outer_key = $this->getMasterKeyMaterial($outer_key_name);
$payload = $this->decryptData($outer_payload, $outer_key, $outer_iv);
$payload = phutil_json_decode($payload);
$inner_iv = $payload['iv.base64'];
$inner_iv = base64_decode($inner_iv);
$inner_iv = new PhutilOpaqueEnvelope($inner_iv);
$inner_key = $payload['key.base64'];
$inner_key = base64_decode($inner_key);
$inner_key = new PhutilOpaqueEnvelope($inner_key);
return array($inner_key, $inner_iv);
}
private function encryptData(
$data,
PhutilOpaqueEnvelope $key,
PhutilOpaqueEnvelope $iv) {
$method = 'aes-256-cbc';
$key = $key->openEnvelope();
$iv = $iv->openEnvelope();
$result = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
if ($result === false) {
throw new Exception(
pht(
'Failed to openssl_encrypt() data: %s',
openssl_error_string()));
}
return $result;
}
private function decryptData(
$data,
PhutilOpaqueEnvelope $key,
PhutilOpaqueEnvelope $iv) {
$method = 'aes-256-cbc';
$key = $key->openEnvelope();
$iv = $iv->openEnvelope();
$result = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
if ($result === false) {
throw new Exception(
pht(
'Failed to openssl_decrypt() data: %s',
openssl_error_string()));
}
return $result;
}
public static function newAES256Key() {
// Unsurprisingly, AES256 uses a 256 bit key.
$key = Filesystem::readRandomBytes(phutil_units('256 bits in bytes'));
return new PhutilOpaqueEnvelope($key);
}
public static function newAES256IV() {
// AES256 uses a 256 bit key, but the initialization vector length is
// only 128 bits.
$iv = Filesystem::readRandomBytes(phutil_units('128 bits in bytes'));
return new PhutilOpaqueEnvelope($iv);
}
public function selectMasterKey($key_name) {
// Require that the key exist on the key ring.
$this->getMasterKeyMaterial($key_name);
$this->keyName = $key_name;
return $this;
}
private function getMasterKeyName() {
if ($this->keyName !== null) {
return $this->keyName;
}
$default = PhabricatorKeyring::getDefaultKeyName(self::FORMATKEY);
if ($default !== null) {
return $default;
}
throw new Exception(
pht(
'No AES256 key is specified in the keyring as a default encryption '.
'key, and no encryption key has been explicitly selected.'));
}
private function getMasterKeyMaterial($key_name) {
return PhabricatorKeyring::getKey($key_name, self::FORMATKEY);
}
}