@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
at upstream/main 215 lines 6.2 kB view raw
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}