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