Remove product literal strings in "pht()", part 5
[phabricator.git] / src / applications / files / format / PhabricatorFileAES256StorageFormat.php
blob1990a2509d1a44e46927c44d8a0425b881cef18d
1 <?php
3 /**
4 * At-rest encryption format using AES256 CBC.
5 */
6 final class PhabricatorFileAES256StorageFormat
7 extends PhabricatorFileStorageFormat {
9 const FORMATKEY = 'aes-256-cbc';
11 private $keyName;
13 public function getStorageFormatName() {
14 return pht('Encrypted (AES-256-CBC)');
17 public function canGenerateNewKeyMaterial() {
18 return true;
21 public function generateNewKeyMaterial() {
22 $envelope = self::newAES256Key();
23 $material = $envelope->openEnvelope();
24 return base64_encode($material);
27 public function canCycleMasterKey() {
28 return true;
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);
45 return array($data);
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);
56 return array($data);
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(
71 $input,
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.
88 $data = array(
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);
102 return array(
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(
135 $data,
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) {
145 throw new Exception(
146 pht(
147 'Failed to openssl_encrypt() data: %s',
148 openssl_error_string()));
151 return $result;
154 private function decryptData(
155 $data,
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) {
165 throw new Exception(
166 pht(
167 'Failed to openssl_decrypt() data: %s',
168 openssl_error_string()));
171 return $result;
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
182 // only 128 bits.
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;
192 return $this;
195 private function getMasterKeyName() {
196 if ($this->keyName !== null) {
197 return $this->keyName;
200 $default = PhabricatorKeyring::getDefaultKeyName(self::FORMATKEY);
201 if ($default !== null) {
202 return $default;
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.'));
211 private function getMasterKeyMaterial($key_name) {
212 return PhabricatorKeyring::getKey($key_name, self::FORMATKEY);