Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15465309
D16124.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
8 KB
Referenced Files
None
Subscribers
None
D16124.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Apr 3 2025, 5:06 PM (5 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7665347
Default Alt Text
D16124.diff (8 KB)
Attached To
Mode
D16124: Add an AES256 storage format for at-rest encryption
Attached
Detach File
Event Timeline
Log In to Comment