Page MenuHomePhabricator

D16124.id.diff
No OneTemporary

D16124.id.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2468,6 +2468,7 @@
'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
+ 'PhabricatorFileAES256StorageFormat' => 'applications/files/format/PhabricatorFileAES256StorageFormat.php',
'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
@@ -7102,6 +7103,7 @@
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
+ 'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileBundleLoader' => 'Phobject',
'PhabricatorFileChunk' => array(
'PhabricatorFileDAO',
diff --git a/src/applications/files/format/PhabricatorFileAES256StorageFormat.php b/src/applications/files/format/PhabricatorFileAES256StorageFormat.php
new file mode 100644
--- /dev/null
+++ b/src/applications/files/format/PhabricatorFileAES256StorageFormat.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * At-rest encryption format using AES256 CBC.
+ */
+final class PhabricatorFileAES256StorageFormat
+ extends PhabricatorFileStorageFormat {
+
+ const FORMATKEY = 'aes-256-cbc';
+
+ private $keyName;
+ private static $keyRing = array();
+
+ public function getStorageFormatName() {
+ return pht('Encrypted (AES-256-CBC)');
+ }
+
+ public function newReadIterator($raw_iterator) {
+ $file = $this->getFile();
+ $data = $file->loadDataFromIterator($raw_iterator);
+
+ list($key, $iv) = $this->extractKeyAndIV($file);
+
+ $data = $this->decryptData($data, $key, $iv);
+
+ return array($data);
+ }
+
+ public function newWriteIterator($raw_iterator) {
+ $file = $this->getFile();
+ $data = $file->loadDataFromIterator($raw_iterator);
+
+ list($key, $iv) = $this->extractKeyAndIV($file);
+
+ $data = $this->encryptData($data, $key, $iv);
+
+ return array($data);
+ }
+
+ public function newStorageProperties() {
+ // Generate a unique key and IV for this block of data.
+ $key_envelope = self::newAES256Key();
+ $iv_envelope = self::newAES256IV();
+
+ // Encode the raw binary data with base64 so we can wrap it in JSON.
+ $data = array(
+ 'iv.base64' => base64_encode($iv_envelope->openEnvelope()),
+ 'key.base64' => base64_encode($key_envelope->openEnvelope()),
+ );
+
+ // Encode the base64 data with JSON.
+ $data_clear = phutil_json_encode($data);
+
+ // Encrypt the block key with the master key, using a unique IV.
+ $data_iv = self::newAES256IV();
+ $key_name = $this->getMasterKeyName();
+ $master_key = self::getMasterKeyFromKeyRing($key_name);
+ $data_cipher = $this->encryptData($data_clear, $master_key, $data_iv);
+
+ return array(
+ 'key.name' => $key_name,
+ 'iv.base64' => base64_encode($data_iv->openEnvelope()),
+ 'payload.base64' => base64_encode($data_cipher),
+ );
+ }
+
+ private function extractKeyAndIV(PhabricatorFile $file) {
+ $outer_iv = $file->getStorageProperty('iv.base64');
+ $outer_iv = base64_decode($outer_iv);
+ $outer_iv = new PhutilOpaqueEnvelope($outer_iv);
+
+ $outer_payload = $file->getStorageProperty('payload.base64');
+ $outer_payload = base64_decode($outer_payload);
+
+ $outer_key_name = $file->getStorageProperty('key.name');
+ $outer_key = self::getMasterKeyFromKeyRing($outer_key_name);
+
+ $payload = $this->decryptData($outer_payload, $outer_key, $outer_iv);
+ $payload = phutil_json_decode($payload);
+
+ $inner_iv = $payload['iv.base64'];
+ $inner_iv = base64_decode($inner_iv);
+ $inner_iv = new PhutilOpaqueEnvelope($inner_iv);
+
+ $inner_key = $payload['key.base64'];
+ $inner_key = base64_decode($inner_key);
+ $inner_key = new PhutilOpaqueEnvelope($inner_key);
+
+ return array($inner_key, $inner_iv);
+ }
+
+ private function encryptData(
+ $data,
+ PhutilOpaqueEnvelope $key,
+ PhutilOpaqueEnvelope $iv) {
+
+ $method = 'aes-256-cbc';
+ $key = $key->openEnvelope();
+ $iv = $iv->openEnvelope();
+
+ $result = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
+ if ($result === false) {
+ throw new Exception(
+ pht(
+ 'Failed to openssl_encrypt() data: %s',
+ openssl_error_string()));
+ }
+
+ return $result;
+ }
+
+ private function decryptData(
+ $data,
+ PhutilOpaqueEnvelope $key,
+ PhutilOpaqueEnvelope $iv) {
+
+ $method = 'aes-256-cbc';
+ $key = $key->openEnvelope();
+ $iv = $iv->openEnvelope();
+
+ $result = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
+ if ($result === false) {
+ throw new Exception(
+ pht(
+ 'Failed to openssl_decrypt() data: %s',
+ openssl_error_string()));
+ }
+
+ return $result;
+ }
+
+ public static function newAES256Key() {
+ // Unsurprisingly, AES256 uses a 256 bit key.
+ $key = Filesystem::readRandomBytes(phutil_units('256 bits in bytes'));
+ return new PhutilOpaqueEnvelope($key);
+ }
+
+ public static function newAES256IV() {
+ // AES256 uses a 256 bit key, but the initialization vector length is
+ // only 128 bits.
+ $iv = Filesystem::readRandomBytes(phutil_units('128 bits in bytes'));
+ return new PhutilOpaqueEnvelope($iv);
+ }
+
+ public function selectKey($key_name) {
+ // Require that the key exist on the key ring.
+ self::getMasterKeyFromKeyRing($key_name);
+
+ $this->keyName = $key_name;
+ return $this;
+ }
+
+ public static function addKeyToKeyRing($name, PhutilOpaqueEnvelope $key) {
+ self::$keyRing[$name] = $key;
+ }
+
+ private function getMasterKeyName() {
+ if ($this->keyName === null) {
+ throw new Exception(pht('No master key selected for AES256 storage.'));
+ }
+
+ return $this->keyName;
+ }
+
+ private static function getMasterKeyFromKeyRing($key_name) {
+ if (!isset(self::$keyRing[$key_name])) {
+ throw new Exception(
+ pht(
+ 'No master key "%s" exists in key ring for AES256 storage.',
+ $key_name));
+ }
+
+ return self::$keyRing[$key_name];
+ }
+
+}
diff --git a/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php b/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
--- a/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
+++ b/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
@@ -35,4 +35,38 @@
$this->assertEqual($expect, $raw_data);
}
+ public function testAES256Storage() {
+ $engine = new PhabricatorTestStorageEngine();
+
+ $key_name = 'test.abcd';
+ $key_text = new PhutilOpaqueEnvelope('abcdefghijklmnopABCDEFGHIJKLMNOP');
+
+ PhabricatorFileAES256StorageFormat::addKeyToKeyRing($key_name, $key_text);
+
+ $format = id(new PhabricatorFileAES256StorageFormat())
+ ->selectKey($key_name);
+
+ $data = 'The cow jumped over the full moon.';
+
+ $params = array(
+ 'name' => 'test.dat',
+ 'storageEngines' => array(
+ $engine,
+ ),
+ 'format' => $format,
+ );
+
+ $file = PhabricatorFile::newFromFileData($data, $params);
+
+ // We should have a file stored as AES256.
+ $format_key = $format->getStorageFormatKey();
+ $this->assertEqual($format_key, $file->getStorageFormat());
+ $this->assertEqual($data, $file->loadFileData());
+
+ // The actual raw data in the storage engine should be encrypted. We
+ // can't really test this, but we can make sure it's not the same as the
+ // input data.
+ $raw_data = $engine->readFile($file->getStorageHandle());
+ $this->assertTrue($data !== $raw_data);
+ }
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -327,10 +327,17 @@
$file = self::initializeNewFile();
$default_key = PhabricatorFileRawStorageFormat::FORMATKEY;
- $format_key = idx($params, 'format', $default_key);
+ $key = idx($params, 'format', $default_key);
- $format = id(clone PhabricatorFileStorageFormat::requireFormat($format_key))
- ->setFile($file);
+ // Callers can pass in an object explicitly instead of a key. This is
+ // primarily useful for unit tests.
+ if ($key instanceof PhabricatorFileStorageFormat) {
+ $format = clone $key;
+ } else {
+ $format = clone PhabricatorFileStorageFormat::requireFormat($key);
+ }
+
+ $format->setFile($file);
$properties = $format->newStorageProperties();
$file->setStorageFormat($format->getStorageFormatKey());

File Metadata

Mime Type
text/plain
Expires
Mar 30 2025, 9:00 AM (5 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7665347
Default Alt Text
D16124.id.diff (8 KB)

Event Timeline