/src/applications/files/format/PhabricatorFileAES256StorageFormat.php

https://github.com/dannysu/phabricator · PHP · 199 lines · 141 code · 47 blank · 11 comment · 4 complexity · ddc860d3ea8393590c87d63f0bb7d731 MD5 · raw file

  1. <?php
  2. /**
  3. * At-rest encryption format using AES256 CBC.
  4. */
  5. final class PhabricatorFileAES256StorageFormat
  6. extends PhabricatorFileStorageFormat {
  7. const FORMATKEY = 'aes-256-cbc';
  8. private $keyName;
  9. public function getStorageFormatName() {
  10. return pht('Encrypted (AES-256-CBC)');
  11. }
  12. public function canGenerateNewKeyMaterial() {
  13. return true;
  14. }
  15. public function generateNewKeyMaterial() {
  16. $envelope = self::newAES256Key();
  17. $material = $envelope->openEnvelope();
  18. return base64_encode($material);
  19. }
  20. public function canCycleMasterKey() {
  21. return true;
  22. }
  23. public function cycleStorageProperties() {
  24. $file = $this->getFile();
  25. list($key, $iv) = $this->extractKeyAndIV($file);
  26. return $this->formatStorageProperties($key, $iv);
  27. }
  28. public function newReadIterator($raw_iterator) {
  29. $file = $this->getFile();
  30. $data = $file->loadDataFromIterator($raw_iterator);
  31. list($key, $iv) = $this->extractKeyAndIV($file);
  32. $data = $this->decryptData($data, $key, $iv);
  33. return array($data);
  34. }
  35. public function newWriteIterator($raw_iterator) {
  36. $file = $this->getFile();
  37. $data = $file->loadDataFromIterator($raw_iterator);
  38. list($key, $iv) = $this->extractKeyAndIV($file);
  39. $data = $this->encryptData($data, $key, $iv);
  40. return array($data);
  41. }
  42. public function newStorageProperties() {
  43. // Generate a unique key and IV for this block of data.
  44. $key_envelope = self::newAES256Key();
  45. $iv_envelope = self::newAES256IV();
  46. return $this->formatStorageProperties($key_envelope, $iv_envelope);
  47. }
  48. private function formatStorageProperties(
  49. PhutilOpaqueEnvelope $key_envelope,
  50. PhutilOpaqueEnvelope $iv_envelope) {
  51. // Encode the raw binary data with base64 so we can wrap it in JSON.
  52. $data = array(
  53. 'iv.base64' => base64_encode($iv_envelope->openEnvelope()),
  54. 'key.base64' => base64_encode($key_envelope->openEnvelope()),
  55. );
  56. // Encode the base64 data with JSON.
  57. $data_clear = phutil_json_encode($data);
  58. // Encrypt the block key with the master key, using a unique IV.
  59. $data_iv = self::newAES256IV();
  60. $key_name = $this->getMasterKeyName();
  61. $master_key = $this->getMasterKeyMaterial($key_name);
  62. $data_cipher = $this->encryptData($data_clear, $master_key, $data_iv);
  63. return array(
  64. 'key.name' => $key_name,
  65. 'iv.base64' => base64_encode($data_iv->openEnvelope()),
  66. 'payload.base64' => base64_encode($data_cipher),
  67. );
  68. }
  69. private function extractKeyAndIV(PhabricatorFile $file) {
  70. $outer_iv = $file->getStorageProperty('iv.base64');
  71. $outer_iv = base64_decode($outer_iv);
  72. $outer_iv = new PhutilOpaqueEnvelope($outer_iv);
  73. $outer_payload = $file->getStorageProperty('payload.base64');
  74. $outer_payload = base64_decode($outer_payload);
  75. $outer_key_name = $file->getStorageProperty('key.name');
  76. $outer_key = $this->getMasterKeyMaterial($outer_key_name);
  77. $payload = $this->decryptData($outer_payload, $outer_key, $outer_iv);
  78. $payload = phutil_json_decode($payload);
  79. $inner_iv = $payload['iv.base64'];
  80. $inner_iv = base64_decode($inner_iv);
  81. $inner_iv = new PhutilOpaqueEnvelope($inner_iv);
  82. $inner_key = $payload['key.base64'];
  83. $inner_key = base64_decode($inner_key);
  84. $inner_key = new PhutilOpaqueEnvelope($inner_key);
  85. return array($inner_key, $inner_iv);
  86. }
  87. private function encryptData(
  88. $data,
  89. PhutilOpaqueEnvelope $key,
  90. PhutilOpaqueEnvelope $iv) {
  91. $method = 'aes-256-cbc';
  92. $key = $key->openEnvelope();
  93. $iv = $iv->openEnvelope();
  94. $result = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
  95. if ($result === false) {
  96. throw new Exception(
  97. pht(
  98. 'Failed to openssl_encrypt() data: %s',
  99. openssl_error_string()));
  100. }
  101. return $result;
  102. }
  103. private function decryptData(
  104. $data,
  105. PhutilOpaqueEnvelope $key,
  106. PhutilOpaqueEnvelope $iv) {
  107. $method = 'aes-256-cbc';
  108. $key = $key->openEnvelope();
  109. $iv = $iv->openEnvelope();
  110. $result = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
  111. if ($result === false) {
  112. throw new Exception(
  113. pht(
  114. 'Failed to openssl_decrypt() data: %s',
  115. openssl_error_string()));
  116. }
  117. return $result;
  118. }
  119. public static function newAES256Key() {
  120. // Unsurprisingly, AES256 uses a 256 bit key.
  121. $key = Filesystem::readRandomBytes(phutil_units('256 bits in bytes'));
  122. return new PhutilOpaqueEnvelope($key);
  123. }
  124. public static function newAES256IV() {
  125. // AES256 uses a 256 bit key, but the initialization vector length is
  126. // only 128 bits.
  127. $iv = Filesystem::readRandomBytes(phutil_units('128 bits in bytes'));
  128. return new PhutilOpaqueEnvelope($iv);
  129. }
  130. public function selectMasterKey($key_name) {
  131. // Require that the key exist on the key ring.
  132. $this->getMasterKeyMaterial($key_name);
  133. $this->keyName = $key_name;
  134. return $this;
  135. }
  136. private function getMasterKeyName() {
  137. if ($this->keyName !== null) {
  138. return $this->keyName;
  139. }
  140. $default = PhabricatorKeyring::getDefaultKeyName(self::FORMATKEY);
  141. if ($default !== null) {
  142. return $default;
  143. }
  144. throw new Exception(
  145. pht(
  146. 'No AES256 key is specified in the keyring as a default encryption '.
  147. 'key, and no encryption key has been explicitly selected.'));
  148. }
  149. private function getMasterKeyMaterial($key_name) {
  150. return PhabricatorKeyring::getKey($key_name, self::FORMATKEY);
  151. }
  152. }