4 * At-rest encryption format using AES256 CBC.
6 final class PhabricatorFileAES256StorageFormat
7 extends PhabricatorFileStorageFormat
{
9 const FORMATKEY
= 'aes-256-cbc';
13 public function getStorageFormatName() {
14 return pht('Encrypted (AES-256-CBC)');
17 public function canGenerateNewKeyMaterial() {
21 public function generateNewKeyMaterial() {
22 $envelope = self
::newAES256Key();
23 $material = $envelope->openEnvelope();
24 return base64_encode($material);
27 public function canCycleMasterKey() {
31 public function cycleStorageProperties() {
32 $file = $this->getFile();
33 list($key, $iv) = $this->extractKeyAndIV($file);
34 return $this->formatStorageProperties($key, $iv);
37 public function newReadIterator($raw_iterator) {
38 $file = $this->getFile();
39 $data = $file->loadDataFromIterator($raw_iterator);
41 list($key, $iv) = $this->extractKeyAndIV($file);
43 $data = $this->decryptData($data, $key, $iv);
48 public function newWriteIterator($raw_iterator) {
49 $file = $this->getFile();
50 $data = $file->loadDataFromIterator($raw_iterator);
52 list($key, $iv) = $this->extractKeyAndIV($file);
54 $data = $this->encryptData($data, $key, $iv);
59 public function newFormatIntegrityHash() {
60 $file = $this->getFile();
61 list($key_envelope, $iv_envelope) = $this->extractKeyAndIV($file);
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.)
68 $input = self
::FORMATKEY
.'/iv:'.$iv_envelope->openEnvelope();
70 return PhabricatorHash
::digestWithNamedKey(
72 PhabricatorFileStorageEngine
::HMAC_INTEGRITY
);
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();
80 return $this->formatStorageProperties($key_envelope, $iv_envelope);
83 private function formatStorageProperties(
84 PhutilOpaqueEnvelope
$key_envelope,
85 PhutilOpaqueEnvelope
$iv_envelope) {
87 // Encode the raw binary data with base64 so we can wrap it in JSON.
89 'iv.base64' => base64_encode($iv_envelope->openEnvelope()),
90 'key.base64' => base64_encode($key_envelope->openEnvelope()),
93 // Encode the base64 data with JSON.
94 $data_clear = phutil_json_encode($data);
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);
103 'key.name' => $key_name,
104 'iv.base64' => base64_encode($data_iv->openEnvelope()),
105 'payload.base64' => base64_encode($data_cipher),
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);
114 $outer_payload = $file->getStorageProperty('payload.base64');
115 $outer_payload = base64_decode($outer_payload);
117 $outer_key_name = $file->getStorageProperty('key.name');
118 $outer_key = $this->getMasterKeyMaterial($outer_key_name);
120 $payload = $this->decryptData($outer_payload, $outer_key, $outer_iv);
121 $payload = phutil_json_decode($payload);
123 $inner_iv = $payload['iv.base64'];
124 $inner_iv = base64_decode($inner_iv);
125 $inner_iv = new PhutilOpaqueEnvelope($inner_iv);
127 $inner_key = $payload['key.base64'];
128 $inner_key = base64_decode($inner_key);
129 $inner_key = new PhutilOpaqueEnvelope($inner_key);
131 return array($inner_key, $inner_iv);
134 private function encryptData(
136 PhutilOpaqueEnvelope
$key,
137 PhutilOpaqueEnvelope
$iv) {
139 $method = 'aes-256-cbc';
140 $key = $key->openEnvelope();
141 $iv = $iv->openEnvelope();
143 $result = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA
, $iv);
144 if ($result === false) {
147 'Failed to openssl_encrypt() data: %s',
148 openssl_error_string()));
154 private function decryptData(
156 PhutilOpaqueEnvelope
$key,
157 PhutilOpaqueEnvelope
$iv) {
159 $method = 'aes-256-cbc';
160 $key = $key->openEnvelope();
161 $iv = $iv->openEnvelope();
163 $result = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA
, $iv);
164 if ($result === false) {
167 'Failed to openssl_decrypt() data: %s',
168 openssl_error_string()));
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);
180 public static function newAES256IV() {
181 // AES256 uses a 256 bit key, but the initialization vector length is
183 $iv = Filesystem
::readRandomBytes(phutil_units('128 bits in bytes'));
184 return new PhutilOpaqueEnvelope($iv);
187 public function selectMasterKey($key_name) {
188 // Require that the key exist on the key ring.
189 $this->getMasterKeyMaterial($key_name);
191 $this->keyName
= $key_name;
195 private function getMasterKeyName() {
196 if ($this->keyName
!== null) {
197 return $this->keyName
;
200 $default = PhabricatorKeyring
::getDefaultKeyName(self
::FORMATKEY
);
201 if ($default !== null) {
207 'No AES256 key is specified in the keyring as a default encryption '.
208 'key, and no encryption key has been explicitly selected.'));
211 private function getMasterKeyMaterial($key_name) {
212 return PhabricatorKeyring
::getKey($key_name, self
::FORMATKEY
);