summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/encryption/appinfo/application.php3
-rw-r--r--apps/encryption/appinfo/register_command.php3
-rw-r--r--apps/encryption/lib/crypto/crypt.php247
-rw-r--r--apps/encryption/lib/crypto/encryption.php84
-rw-r--r--apps/encryption/lib/keymanager.php34
-rw-r--r--apps/encryption/settings/settings-admin.php3
-rw-r--r--apps/encryption/settings/settings-personal.php3
-rw-r--r--apps/encryption/tests/lib/KeyManagerTest.php67
-rw-r--r--apps/encryption/tests/lib/crypto/cryptTest.php104
-rw-r--r--apps/encryption/tests/lib/crypto/encryptionTest.php2
-rw-r--r--apps/files_versions/lib/storage.php10
-rw-r--r--lib/private/files/cache/cache.php9
-rw-r--r--lib/private/files/fileinfo.php9
-rw-r--r--lib/private/files/storage/wrapper/encryption.php16
-rw-r--r--lib/private/files/stream/encryption.php31
-rw-r--r--lib/public/encryption/iencryptionmodule.php7
-rw-r--r--settings/changepassword/controller.php3
-rw-r--r--tests/lib/files/stream/encryption.php1
18 files changed, 499 insertions, 137 deletions
diff --git a/apps/encryption/appinfo/application.php b/apps/encryption/appinfo/application.php
index 433e9e86284..6d01d3e8353 100644
--- a/apps/encryption/appinfo/application.php
+++ b/apps/encryption/appinfo/application.php
@@ -131,7 +131,8 @@ class Application extends \OCP\AppFramework\App {
$server = $c->getServer();
return new Crypt($server->getLogger(),
$server->getUserSession(),
- $server->getConfig());
+ $server->getConfig(),
+ $server->getL10N($c->getAppName()));
});
$container->registerService('Session',
diff --git a/apps/encryption/appinfo/register_command.php b/apps/encryption/appinfo/register_command.php
index 2bb49d55c2e..5f32718cdf0 100644
--- a/apps/encryption/appinfo/register_command.php
+++ b/apps/encryption/appinfo/register_command.php
@@ -25,11 +25,12 @@ use Symfony\Component\Console\Helper\QuestionHelper;
$userManager = OC::$server->getUserManager();
$view = new \OC\Files\View();
$config = \OC::$server->getConfig();
+$l = \OC::$server->getL10N('encryption');
$userSession = \OC::$server->getUserSession();
$connection = \OC::$server->getDatabaseConnection();
$logger = \OC::$server->getLogger();
$questionHelper = new QuestionHelper();
-$crypt = new \OCA\Encryption\Crypto\Crypt($logger, $userSession, $config);
+$crypt = new \OCA\Encryption\Crypto\Crypt($logger, $userSession, $config, $l);
$util = new \OCA\Encryption\Util($view, $crypt, $logger, $userSession, $config, $userManager);
$application->add(new MigrateKeys($userManager, $view, $connection, $config, $logger));
diff --git a/apps/encryption/lib/crypto/crypt.php b/apps/encryption/lib/crypto/crypt.php
index e387380cd95..b4c10f42790 100644
--- a/apps/encryption/lib/crypto/crypt.php
+++ b/apps/encryption/lib/crypto/crypt.php
@@ -29,16 +29,32 @@ namespace OCA\Encryption\Crypto;
use OC\Encryption\Exceptions\DecryptionFailedException;
use OC\Encryption\Exceptions\EncryptionFailedException;
+use OC\HintException;
use OCA\Encryption\Exceptions\MultiKeyDecryptException;
use OCA\Encryption\Exceptions\MultiKeyEncryptException;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\IConfig;
+use OCP\IL10N;
use OCP\ILogger;
use OCP\IUserSession;
+/**
+ * Class Crypt provides the encryption implementation of the default ownCloud
+ * encryption module. As default AES-256-CTR is used, it does however offer support
+ * for the following modes:
+ *
+ * - AES-256-CTR
+ * - AES-128-CTR
+ * - AES-256-CFB
+ * - AES-128-CFB
+ *
+ * For integrity protection Encrypt-Then-MAC using HMAC-SHA256 is used.
+ *
+ * @package OCA\Encryption\Crypto
+ */
class Crypt {
- const DEFAULT_CIPHER = 'AES-256-CFB';
+ const DEFAULT_CIPHER = 'AES-256-CTR';
// default cipher from old ownCloud versions
const LEGACY_CIPHER = 'AES-128-CFB';
@@ -48,33 +64,41 @@ class Crypt {
const HEADER_START = 'HBEGIN';
const HEADER_END = 'HEND';
- /**
- * @var ILogger
- */
+
+ /** @var ILogger */
private $logger;
- /**
- * @var string
- */
+
+ /** @var string */
private $user;
- /**
- * @var IConfig
- */
+
+ /** @var IConfig */
private $config;
- /**
- * @var array
- */
+ /** @var array */
private $supportedKeyFormats;
+ /** @var IL10N */
+ private $l;
+
+ /** @var array */
+ private $supportedCiphersAndKeySize = [
+ 'AES-256-CTR' => 32,
+ 'AES-128-CTR' => 16,
+ 'AES-256-CFB' => 32,
+ 'AES-128-CFB' => 16,
+ ];
+
/**
* @param ILogger $logger
* @param IUserSession $userSession
* @param IConfig $config
+ * @param IL10N $l
*/
- public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config) {
+ public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l) {
$this->logger = $logger;
$this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : '"no user given"';
$this->config = $config;
+ $this->l = $l;
$this->supportedKeyFormats = ['hash', 'password'];
}
@@ -145,10 +169,12 @@ class Crypt {
/**
* @param string $plainContent
* @param string $passPhrase
+ * @param int $version
+ * @param int $position
* @return false|string
- * @throws GenericEncryptionException
+ * @throws EncryptionFailedException
*/
- public function symmetricEncryptFileContent($plainContent, $passPhrase) {
+ public function symmetricEncryptFileContent($plainContent, $passPhrase, $version, $position) {
if (!$plainContent) {
$this->logger->error('Encryption Library, symmetrical encryption failed no content given',
@@ -162,8 +188,13 @@ class Crypt {
$iv,
$passPhrase,
$this->getCipher());
+
+ // Create a signature based on the key as well as the current version
+ $sig = $this->createSignature($encryptedContent, $passPhrase.$version.$position);
+
// combine content to encrypt the IV identifier and actual IV
$catFile = $this->concatIV($encryptedContent, $iv);
+ $catFile = $this->concatSig($catFile, $sig);
$padded = $this->addPadding($catFile);
return $padded;
@@ -225,8 +256,13 @@ class Crypt {
*/
public function getCipher() {
$cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER);
- if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') {
- $this->logger->warning('Wrong cipher defined in config.php only AES-128-CFB and AES-256-CFB are supported. Fall back' . self::DEFAULT_CIPHER,
+ if (!isset($this->supportedCiphersAndKeySize[$cipher])) {
+ $this->logger->warning(
+ sprintf(
+ 'Unsupported cipher (%s) defined in config.php supported. Falling back to %s',
+ $cipher,
+ self::DEFAULT_CIPHER
+ ),
['app' => 'encryption']);
$cipher = self::DEFAULT_CIPHER;
}
@@ -237,19 +273,20 @@ class Crypt {
/**
* get key size depending on the cipher
*
- * @param string $cipher supported ('AES-256-CFB' and 'AES-128-CFB')
+ * @param string $cipher
* @return int
* @throws \InvalidArgumentException
*/
protected function getKeySize($cipher) {
- if ($cipher === 'AES-256-CFB') {
- return 32;
- } else if ($cipher === 'AES-128-CFB') {
- return 16;
+ if(isset($this->supportedCiphersAndKeySize[$cipher])) {
+ return $this->supportedCiphersAndKeySize[$cipher];
}
throw new \InvalidArgumentException(
- 'Wrong cipher defined only AES-128-CFB and AES-256-CFB are supported.'
+ sprintf(
+ 'Unsupported cipher (%s) defined.',
+ $cipher
+ )
);
}
@@ -272,11 +309,24 @@ class Crypt {
}
/**
+ * @param string $encryptedContent
+ * @param string $signature
+ * @return string
+ */
+ private function concatSig($encryptedContent, $signature) {
+ return $encryptedContent . '00sig00' . $signature;
+ }
+
+ /**
+ * Note: This is _NOT_ a padding used for encryption purposes. It is solely
+ * used to achieve the PHP stream size. It has _NOTHING_ to do with the
+ * encrypted content and is not used in any crypto primitive.
+ *
* @param string $data
* @return string
*/
private function addPadding($data) {
- return $data . 'xx';
+ return $data . 'xxx';
}
/**
@@ -318,7 +368,9 @@ class Crypt {
$hash = $this->generatePasswordHash($password, $cipher, $uid);
$encryptedKey = $this->symmetricEncryptFileContent(
$privateKey,
- $hash
+ $hash,
+ 0,
+ 0
);
return $encryptedKey;
@@ -357,9 +409,12 @@ class Crypt {
self::HEADER_END) + strlen(self::HEADER_END));
}
- $plainKey = $this->symmetricDecryptFileContent($privateKey,
+ $plainKey = $this->symmetricDecryptFileContent(
+ $privateKey,
$password,
- $cipher);
+ $cipher,
+ 0
+ );
if ($this->isValidPrivateKey($plainKey) === false) {
return false;
@@ -390,14 +445,17 @@ class Crypt {
* @param string $keyFileContents
* @param string $passPhrase
* @param string $cipher
+ * @param int $version
+ * @param int $position
* @return string
* @throws DecryptionFailedException
*/
- public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER) {
- // Remove Padding
- $noPadding = $this->removePadding($keyFileContents);
+ public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
+ $catFile = $this->splitMetaData($keyFileContents, $cipher);
- $catFile = $this->splitIv($noPadding);
+ if ($catFile['signature'] !== false) {
+ $this->checkSignature($catFile['encrypted'], $passPhrase.$version.$position, $catFile['signature']);
+ }
return $this->decrypt($catFile['encrypted'],
$catFile['iv'],
@@ -406,42 +464,103 @@ class Crypt {
}
/**
- * remove padding
+ * check for valid signature
*
- * @param $padded
- * @return string|false
+ * @param string $data
+ * @param string $passPhrase
+ * @param string $expectedSignature
+ * @throws HintException
*/
- private function removePadding($padded) {
- if (substr($padded, -2) === 'xx') {
- return substr($padded, 0, -2);
+ private function checkSignature($data, $passPhrase, $expectedSignature) {
+ $signature = $this->createSignature($data, $passPhrase);
+ if (!hash_equals($expectedSignature, $signature)) {
+ throw new HintException('Bad Signature', $this->l->t('Bad Signature'));
}
- return false;
}
/**
- * split iv from encrypted content
+ * create signature
*
- * @param string|false $catFile
+ * @param string $data
+ * @param string $passPhrase
* @return string
*/
- private function splitIv($catFile) {
- // Fetch encryption metadata from end of file
- $meta = substr($catFile, -22);
+ private function createSignature($data, $passPhrase) {
+ $passPhrase = hash('sha512', $passPhrase . 'a', true);
+ $signature = hash_hmac('sha256', $data, $passPhrase);
+ return $signature;
+ }
- // Fetch IV from end of file
- $iv = substr($meta, -16);
- // Remove IV and IV Identifier text to expose encrypted content
+ /**
+ * remove padding
+ *
+ * @param string $padded
+ * @param bool $hasSignature did the block contain a signature, in this case we use a different padding
+ * @return string|false
+ */
+ private function removePadding($padded, $hasSignature = false) {
+ if ($hasSignature === false && substr($padded, -2) === 'xx') {
+ return substr($padded, 0, -2);
+ } elseif ($hasSignature === true && substr($padded, -3) === 'xxx') {
+ return substr($padded, 0, -3);
+ }
+ return false;
+ }
- $encrypted = substr($catFile, 0, -22);
+ /**
+ * split meta data from encrypted file
+ * Note: for now, we assume that the meta data always start with the iv
+ * followed by the signature, if available
+ *
+ * @param string $catFile
+ * @param string $cipher
+ * @return array
+ */
+ private function splitMetaData($catFile, $cipher) {
+ if ($this->hasSignature($catFile, $cipher)) {
+ $catFile = $this->removePadding($catFile, true);
+ $meta = substr($catFile, -93);
+ $iv = substr($meta, strlen('00iv00'), 16);
+ $sig = substr($meta, 22 + strlen('00sig00'));
+ $encrypted = substr($catFile, 0, -93);
+ } else {
+ $catFile = $this->removePadding($catFile);
+ $meta = substr($catFile, -22);
+ $iv = substr($meta, -16);
+ $sig = false;
+ $encrypted = substr($catFile, 0, -22);
+ }
return [
'encrypted' => $encrypted,
- 'iv' => $iv
+ 'iv' => $iv,
+ 'signature' => $sig
];
}
/**
+ * check if encrypted block is signed
+ *
+ * @param string $catFile
+ * @param string $cipher
+ * @return bool
+ * @throws HintException
+ */
+ private function hasSignature($catFile, $cipher) {
+ $meta = substr($catFile, -93);
+ $signaturePosition = strpos($meta, '00sig00');
+
+ // enforce signature for the new 'CTR' ciphers
+ if ($signaturePosition === false && strpos(strtolower($cipher), 'ctr') !== false) {
+ throw new HintException('Missing Signature', $this->l->t('Missing Signature'));
+ }
+
+ return ($signaturePosition !== false);
+ }
+
+
+ /**
* @param string $encryptedContent
* @param string $iv
* @param string $passPhrase
@@ -496,40 +615,18 @@ class Crypt {
* @throws GenericEncryptionException
*/
private function generateIv() {
- $random = openssl_random_pseudo_bytes(12, $strong);
- if ($random) {
- if (!$strong) {
- // If OpenSSL indicates randomness is insecure log error
- $this->logger->error('Encryption Library: Insecure symmetric key was generated using openssl_random_psudo_bytes()',
- ['app' => 'encryption']);
- }
-
- /*
- * We encode the iv purely for string manipulation
- * purposes -it gets decoded before use
- */
- return base64_encode($random);
- }
- // If we ever get here we've failed anyway no need for an else
- throw new GenericEncryptionException('Generating IV Failed');
+ return random_bytes(16);
}
/**
- * Generate a cryptographically secure pseudo-random base64 encoded 256-bit
- * ASCII key, used as file key
+ * Generate a cryptographically secure pseudo-random 256-bit ASCII key, used
+ * as file key
*
* @return string
* @throws \Exception
*/
public function generateFileKey() {
- // Generate key
- $key = base64_encode(openssl_random_pseudo_bytes(32, $strong));
- if (!$key || !$strong) {
- // If OpenSSL indicates randomness is insecure, log error
- throw new \Exception('Encryption library, Insecure symmetric key was generated using openssl_random_pseudo_bytes()');
- }
-
- return $key;
+ return random_bytes(32);
}
/**
diff --git a/apps/encryption/lib/crypto/encryption.php b/apps/encryption/lib/crypto/encryption.php
index 3b66684a7f4..a637f52a869 100644
--- a/apps/encryption/lib/crypto/encryption.php
+++ b/apps/encryption/lib/crypto/encryption.php
@@ -29,6 +29,7 @@ namespace OCA\Encryption\Crypto;
use OC\Encryption\Exceptions\DecryptionFailedException;
+use OC\Files\View;
use OCA\Encryption\Exceptions\PublicKeyMissingException;
use OCA\Encryption\Session;
use OCA\Encryption\Util;
@@ -56,6 +57,9 @@ class Encryption implements IEncryptionModule {
private $path;
/** @var string */
+ private $realPath;
+
+ /** @var string */
private $user;
/** @var string */
@@ -94,6 +98,16 @@ class Encryption implements IEncryptionModule {
/** @var DecryptAll */
private $decryptAll;
+ /** @var int unencrypted block size if block contains signature */
+ private $unencryptedBlockSizeSigned = 6072;
+
+ /** @var int unencrypted block size */
+ private $unencryptedBlockSize = 6126;
+
+ /** @var int Current version of the file */
+ private $version = 0;
+
+
/**
*
* @param Crypt $crypt
@@ -156,8 +170,8 @@ class Encryption implements IEncryptionModule {
* or if no additional data is needed return a empty array
*/
public function begin($path, $user, $mode, array $header, array $accessList) {
-
$this->path = $this->getPathToRealFile($path);
+ $this->realPath = $path;
$this->accessList = $accessList;
$this->user = $user;
$this->isWriteOperation = false;
@@ -173,6 +187,8 @@ class Encryption implements IEncryptionModule {
$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
}
+ $this->version = (int)$this->keyManager->getVersion($this->realPath, new View());
+
if (
$mode === 'w'
|| $mode === 'w+'
@@ -185,17 +201,17 @@ class Encryption implements IEncryptionModule {
}
}
- if (isset($header['cipher'])) {
- $this->cipher = $header['cipher'];
- } elseif ($this->isWriteOperation) {
+ if ($this->isWriteOperation) {
$this->cipher = $this->crypt->getCipher();
+ } elseif (isset($header['cipher'])) {
+ $this->cipher = $header['cipher'];
} else {
// if we read a file without a header we fall-back to the legacy cipher
// which was used in <=oC6
$this->cipher = $this->crypt->getLegacyCipher();
}
- return array('cipher' => $this->cipher);
+ return array('cipher' => $this->cipher, 'signed' => 'true');
}
/**
@@ -204,17 +220,25 @@ class Encryption implements IEncryptionModule {
* buffer.
*
* @param string $path to the file
+ * @param int $position
* @return string remained data which should be written to the file in case
* of a write operation
* @throws PublicKeyMissingException
* @throws \Exception
* @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
*/
- public function end($path) {
+ public function end($path, $position = 0) {
$result = '';
if ($this->isWriteOperation) {
+ // Partial files do not increase the version
+ if(\OC\Files\Cache\Scanner::isPartialFile($path)) {
+ $version = $this->version;
+ } else {
+ $version = $this->version + 1;
+ }
+ $this->keyManager->setVersion($this->path, $this->version+1, new View());
if (!empty($this->writeCache)) {
- $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey);
+ $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $version, $position);
$this->writeCache = '';
}
$publicKeys = array();
@@ -248,12 +272,12 @@ class Encryption implements IEncryptionModule {
* encrypt data
*
* @param string $data you want to encrypt
+ * @param int $position
* @return string encrypted data
*/
- public function encrypt($data) {
-
+ public function encrypt($data, $position = 0) {
// If extra data is left over from the last round, make sure it
- // is integrated into the next 6126 / 8192 block
+ // is integrated into the next block
if ($this->writeCache) {
// Concat writeCache to start of $data
@@ -275,7 +299,7 @@ class Encryption implements IEncryptionModule {
// If data remaining to be written is less than the
// size of 1 6126 byte block
- if ($remainingLength < 6126) {
+ if ($remainingLength < $this->unencryptedBlockSizeSigned) {
// Set writeCache to contents of $data
// The writeCache will be carried over to the
@@ -293,14 +317,20 @@ class Encryption implements IEncryptionModule {
} else {
// Read the chunk from the start of $data
- $chunk = substr($data, 0, 6126);
+ $chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
- $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey);
+ // Partial files do not increase the version
+ if(\OC\Files\Cache\Scanner::isPartialFile($this->path)) {
+ $version = $this->version;
+ } else {
+ $version = $this->version + 1;
+ }
+ $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $version, $position);
// Remove the chunk we just processed from
// $data, leaving only unprocessed data in $data
// var, for handling on the next round
- $data = substr($data, 6126);
+ $data = substr($data, $this->unencryptedBlockSizeSigned);
}
@@ -313,10 +343,11 @@ class Encryption implements IEncryptionModule {
* decrypt data
*
* @param string $data you want to decrypt
+ * @param int $position
* @return string decrypted data
* @throws DecryptionFailedException
*/
- public function decrypt($data) {
+ public function decrypt($data, $position = 0) {
if (empty($this->fileKey)) {
$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
$hint = $this->l->t('Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
@@ -325,11 +356,7 @@ class Encryption implements IEncryptionModule {
throw new DecryptionFailedException($msg, $hint);
}
- $result = '';
- if (!empty($data)) {
- $result = $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher);
- }
- return $result;
+ return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
}
/**
@@ -342,6 +369,10 @@ class Encryption implements IEncryptionModule {
*/
public function update($path, $uid, array $accessList) {
$fileKey = $this->keyManager->getFileKey($path, $uid);
+ if(empty($this->realPath)) {
+ $this->realPath = $path;
+ }
+ $version = $this->keyManager->getVersion($this->realPath, new View());
if (!empty($fileKey)) {
@@ -362,6 +393,8 @@ class Encryption implements IEncryptionModule {
$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
+ $this->keyManager->setVersion($path, $version, new View());
+
} else {
$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
array('file' => $path, 'app' => 'encryption'));
@@ -407,10 +440,15 @@ class Encryption implements IEncryptionModule {
* get size of the unencrypted payload per block.
* ownCloud read/write files with a block size of 8192 byte
*
- * @return integer
+ * @param bool $signed
+ * @return int
*/
- public function getUnencryptedBlockSize() {
- return 6126;
+ public function getUnencryptedBlockSize($signed = false) {
+ if ($signed === false) {
+ return $this->unencryptedBlockSize;
+ }
+
+ return $this->unencryptedBlockSizeSigned;
}
/**
diff --git a/apps/encryption/lib/keymanager.php b/apps/encryption/lib/keymanager.php
index b6365cf2cce..0c957e12012 100644
--- a/apps/encryption/lib/keymanager.php
+++ b/apps/encryption/lib/keymanager.php
@@ -25,12 +25,14 @@
namespace OCA\Encryption;
use OC\Encryption\Exceptions\DecryptionFailedException;
+use OC\Files\View;
use OCA\Encryption\Crypto\Encryption;
use OCA\Encryption\Exceptions\PrivateKeyMissingException;
use OCA\Encryption\Exceptions\PublicKeyMissingException;
use OCA\Encryption\Crypto\Crypt;
use OCP\Encryption\Keys\IStorage;
use OCP\IConfig;
+use OCP\IDBConnection;
use OCP\ILogger;
use OCP\IUserSession;
@@ -413,6 +415,37 @@ class KeyManager {
}
/**
+ * Get the current version of a file
+ *
+ * @param string $path
+ * @param View $view
+ * @return int
+ */
+ public function getVersion($path, View $view) {
+ $fileInfo = $view->getFileInfo($path);
+ if($fileInfo === false) {
+ return 0;
+ }
+ return $fileInfo->getEncryptedVersion();
+ }
+
+ /**
+ * Set the current version of a file
+ *
+ * @param string $path
+ * @param int $version
+ * @param View $view
+ */
+ public function setVersion($path, $version, View $view) {
+ $fileInfo= $view->getFileInfo($path);
+
+ if($fileInfo !== false) {
+ $cache = $fileInfo->getStorage()->getCache();
+ $cache->update($fileInfo->getId(), ['encrypted' => $version, 'encryptedVersion' => $version]);
+ }
+ }
+
+ /**
* get the encrypted file key
*
* @param string $path
@@ -546,6 +579,7 @@ class KeyManager {
/**
* @param string $path
+ * @return bool
*/
public function deleteAllFileKeys($path) {
return $this->keyStorage->deleteAllFileKeys($path);
diff --git a/apps/encryption/settings/settings-admin.php b/apps/encryption/settings/settings-admin.php
index c3d523f27da..6c7c0987fd7 100644
--- a/apps/encryption/settings/settings-admin.php
+++ b/apps/encryption/settings/settings-admin.php
@@ -29,7 +29,8 @@ $tmpl = new OCP\Template('encryption', 'settings-admin');
$crypt = new \OCA\Encryption\Crypto\Crypt(
\OC::$server->getLogger(),
\OC::$server->getUserSession(),
- \OC::$server->getConfig());
+ \OC::$server->getConfig(),
+ \OC::$server->getL10N('encryption'));
$util = new \OCA\Encryption\Util(
new \OC\Files\View(),
diff --git a/apps/encryption/settings/settings-personal.php b/apps/encryption/settings/settings-personal.php
index 2dff5904850..0f6e9353707 100644
--- a/apps/encryption/settings/settings-personal.php
+++ b/apps/encryption/settings/settings-personal.php
@@ -28,7 +28,8 @@ $template = new OCP\Template('encryption', 'settings-personal');
$crypt = new \OCA\Encryption\Crypto\Crypt(
\OC::$server->getLogger(),
$userSession,
- \OC::$server->getConfig());
+ \OC::$server->getConfig(),
+ \OC::$server->getL10N('encryption'));
$util = new \OCA\Encryption\Util(
new \OC\Files\View(),
diff --git a/apps/encryption/tests/lib/KeyManagerTest.php b/apps/encryption/tests/lib/KeyManagerTest.php
index c69610fb541..ea1830db4d5 100644
--- a/apps/encryption/tests/lib/KeyManagerTest.php
+++ b/apps/encryption/tests/lib/KeyManagerTest.php
@@ -579,4 +579,71 @@ class KeyManagerTest extends TestCase {
];
}
+ public function testGetVersionWithoutFileInfo() {
+ $view = $this->getMockBuilder('\\OC\\Files\\View')
+ ->disableOriginalConstructor()->getMock();
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('/admin/files/myfile.txt')
+ ->willReturn(false);
+
+ $this->assertSame(0, $this->instance->getVersion('/admin/files/myfile.txt', $view));
+ }
+
+ public function testGetVersionWithFileInfo() {
+ $view = $this->getMockBuilder('\\OC\\Files\\View')
+ ->disableOriginalConstructor()->getMock();
+ $fileInfo = $this->getMockBuilder('\\OC\\Files\\FileInfo')
+ ->disableOriginalConstructor()->getMock();
+ $fileInfo->expects($this->once())
+ ->method('getEncryptedVersion')
+ ->willReturn(1337);
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('/admin/files/myfile.txt')
+ ->willReturn($fileInfo);
+
+ $this->assertSame(1337, $this->instance->getVersion('/admin/files/myfile.txt', $view));
+ }
+
+ public function testSetVersionWithFileInfo() {
+ $view = $this->getMockBuilder('\\OC\\Files\\View')
+ ->disableOriginalConstructor()->getMock();
+ $cache = $this->getMockBuilder('\\OCP\\Files\\Cache\\ICache')
+ ->disableOriginalConstructor()->getMock();
+ $cache->expects($this->once())
+ ->method('update')
+ ->with(123, ['encrypted' => 5, 'encryptedVersion' => 5]);
+ $storage = $this->getMockBuilder('\\OCP\\Files\\Storage')
+ ->disableOriginalConstructor()->getMock();
+ $storage->expects($this->once())
+ ->method('getCache')
+ ->willReturn($cache);
+ $fileInfo = $this->getMockBuilder('\\OC\\Files\\FileInfo')
+ ->disableOriginalConstructor()->getMock();
+ $fileInfo->expects($this->once())
+ ->method('getStorage')
+ ->willReturn($storage);
+ $fileInfo->expects($this->once())
+ ->method('getId')
+ ->willReturn(123);
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('/admin/files/myfile.txt')
+ ->willReturn($fileInfo);
+
+ $this->instance->setVersion('/admin/files/myfile.txt', 5, $view);
+ }
+
+ public function testSetVersionWithoutFileInfo() {
+ $view = $this->getMockBuilder('\\OC\\Files\\View')
+ ->disableOriginalConstructor()->getMock();
+ $view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('/admin/files/myfile.txt')
+ ->willReturn(false);
+
+ $this->instance->setVersion('/admin/files/myfile.txt', 5, $view);
+ }
+
}
diff --git a/apps/encryption/tests/lib/crypto/cryptTest.php b/apps/encryption/tests/lib/crypto/cryptTest.php
index e599cc28963..d94aea463cf 100644
--- a/apps/encryption/tests/lib/crypto/cryptTest.php
+++ b/apps/encryption/tests/lib/crypto/cryptTest.php
@@ -39,6 +39,10 @@ class cryptTest extends TestCase {
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $config;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $l;
+
/** @var Crypt */
private $crypt;
@@ -57,8 +61,9 @@ class cryptTest extends TestCase {
$this->config = $this->getMockBuilder('OCP\IConfig')
->disableOriginalConstructor()
->getMock();
+ $this->l = $this->getMock('OCP\IL10N');
- $this->crypt = new Crypt($this->logger, $this->userSession, $this->config);
+ $this->crypt = new Crypt($this->logger, $this->userSession, $this->config, $this->l);
}
/**
@@ -105,7 +110,7 @@ class cryptTest extends TestCase {
$this->config->expects($this->once())
->method('getSystemValue')
- ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CFB'))
+ ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR'))
->willReturn('AES-128-CFB');
if ($keyFormat) {
@@ -126,6 +131,9 @@ class cryptTest extends TestCase {
$this->crypt->generateHeader('unknown');
}
+ /**
+ * @return array
+ */
public function dataTestGenerateHeader() {
return [
[null, 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:HEND'],
@@ -134,16 +142,28 @@ class cryptTest extends TestCase {
];
}
+ public function testGetCipherWithInvalidCipher() {
+ $this->config->expects($this->once())
+ ->method('getSystemValue')
+ ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR'))
+ ->willReturn('Not-Existing-Cipher');
+ $this->logger
+ ->expects($this->once())
+ ->method('warning')
+ ->with('Unsupported cipher (Not-Existing-Cipher) defined in config.php supported. Falling back to AES-256-CTR');
+
+ $this->assertSame('AES-256-CTR', $this->crypt->getCipher());
+ }
+
/**
* @dataProvider dataProviderGetCipher
* @param string $configValue
* @param string $expected
*/
public function testGetCipher($configValue, $expected) {
-
$this->config->expects($this->once())
->method('getSystemValue')
- ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CFB'))
+ ->with($this->equalTo('cipher'), $this->equalTo('AES-256-CTR'))
->willReturn($configValue);
$this->assertSame($expected,
@@ -161,7 +181,10 @@ class cryptTest extends TestCase {
return array(
array('AES-128-CFB', 'AES-128-CFB'),
array('AES-256-CFB', 'AES-256-CFB'),
- array('unknown', 'AES-256-CFB')
+ array('AES-128-CTR', 'AES-128-CTR'),
+ array('AES-256-CTR', 'AES-256-CTR'),
+
+ array('unknown', 'AES-256-CTR')
);
}
@@ -181,17 +204,61 @@ class cryptTest extends TestCase {
}
/**
- * test splitIV()
+ * @dataProvider dataTestSplitMetaData
*/
- public function testSplitIV() {
- $data = 'encryptedContent00iv001234567890123456';
- $result = self::invokePrivate($this->crypt, 'splitIV', array($data));
+ public function testSplitMetaData($data, $expected) {
+ $result = self::invokePrivate($this->crypt, 'splitMetaData', array($data, 'AES-256-CFB'));
$this->assertTrue(is_array($result));
- $this->assertSame(2, count($result));
+ $this->assertSame(3, count($result));
$this->assertArrayHasKey('encrypted', $result);
$this->assertArrayHasKey('iv', $result);
- $this->assertSame('encryptedContent', $result['encrypted']);
- $this->assertSame('1234567890123456', $result['iv']);
+ $this->assertArrayHasKey('signature', $result);
+ $this->assertSame($expected['encrypted'], $result['encrypted']);
+ $this->assertSame($expected['iv'], $result['iv']);
+ $this->assertSame($expected['signature'], $result['signature']);
+ }
+
+ public function dataTestSplitMetaData() {
+ return [
+ ['encryptedContent00iv001234567890123456xx',
+ ['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => false]],
+ ['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx',
+ ['encrypted' => 'encryptedContent', 'iv' => '1234567890123456', 'signature' => 'e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86']],
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestHasSignature
+ */
+ public function testHasSignature($data, $expected) {
+ $this->assertSame($expected,
+ $this->invokePrivate($this->crypt, 'hasSignature', array($data, 'AES-256-CFB'))
+ );
+ }
+
+ public function dataTestHasSignature() {
+ return [
+ ['encryptedContent00iv001234567890123456xx', false],
+ ['encryptedContent00iv00123456789012345600sig00e1992521e437f6915f9173b190a512cfc38a00ac24502db44e0ba10c2bb0cc86xxx', true]
+ ];
+ }
+
+ /**
+ * @dataProvider dataTestHasSignatureFail
+ * @expectedException \OC\HintException
+ */
+ public function testHasSignatureFail($cipher) {
+ $data = 'encryptedContent00iv001234567890123456xx';
+ $this->invokePrivate($this->crypt, 'hasSignature', array($data, $cipher));
+ }
+
+ public function dataTestHasSignatureFail() {
+ return [
+ ['AES-256-CTR'],
+ ['aes-256-ctr'],
+ ['AES-128-CTR'],
+ ['ctr-256-ctr']
+ ];
}
/**
@@ -199,7 +266,7 @@ class cryptTest extends TestCase {
*/
public function testAddPadding() {
$result = self::invokePrivate($this->crypt, 'addPadding', array('data'));
- $this->assertSame('dataxx', $result);
+ $this->assertSame('dataxxx', $result);
}
/**
@@ -303,10 +370,15 @@ class cryptTest extends TestCase {
$this->invokePrivate($this->crypt, 'getKeySize', ['foo']);
}
+ /**
+ * @return array
+ */
public function dataTestGetKeySize() {
return [
['AES-256-CFB', 32],
['AES-128-CFB', 16],
+ ['AES-256-CTR', 32],
+ ['AES-128-CTR', 16],
];
}
@@ -320,7 +392,8 @@ class cryptTest extends TestCase {
[
$this->logger,
$this->userSession,
- $this->config
+ $this->config,
+ $this->l
]
)
->setMethods(
@@ -351,6 +424,9 @@ class cryptTest extends TestCase {
$this->assertSame($expected, $result);
}
+ /**
+ * @return array
+ */
public function dataTestDecryptPrivateKey() {
return [
[['cipher' => 'AES-128-CFB', 'keyFormat' => 'password'], 'HBEGIN:HENDprivateKey', 'AES-128-CFB', true, 'key'],
diff --git a/apps/encryption/tests/lib/crypto/encryptionTest.php b/apps/encryption/tests/lib/crypto/encryptionTest.php
index 62e77c742d8..ad943ab6e49 100644
--- a/apps/encryption/tests/lib/crypto/encryptionTest.php
+++ b/apps/encryption/tests/lib/crypto/encryptionTest.php
@@ -229,7 +229,7 @@ class EncryptionTest extends TestCase {
public function dataTestBegin() {
return array(
- array('w', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'myCipher'),
+ array('w', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'defaultCipher'),
array('r', ['cipher' => 'myCipher'], 'legacyCipher', 'defaultCipher', 'fileKey', 'myCipher'),
array('w', [], 'legacyCipher', 'defaultCipher', '', 'defaultCipher'),
array('r', [], 'legacyCipher', 'defaultCipher', 'file_key', 'legacyCipher'),
diff --git a/apps/files_versions/lib/storage.php b/apps/files_versions/lib/storage.php
index 47acec1d763..0b121c344f9 100644
--- a/apps/files_versions/lib/storage.php
+++ b/apps/files_versions/lib/storage.php
@@ -165,7 +165,15 @@ class Storage {
$mtime = $users_view->filemtime('files/' . $filename);
$users_view->copy('files/' . $filename, 'files_versions/' . $filename . '.v' . $mtime);
// call getFileInfo to enforce a file cache entry for the new version
- $users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime);
+ $newFileInfo = $users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime);
+
+ // Keep the "encrypted" value of the original file
+ $oldVersion = $files_view->getFileInfo($filename)->getEncryptedVersion();
+ $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder();
+ $qb->update('filecache')
+ ->set('encrypted', $qb->createNamedParameter($oldVersion))
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($newFileInfo->getId())))
+ ->execute();
}
}
diff --git a/lib/private/files/cache/cache.php b/lib/private/files/cache/cache.php
index 22b9f49e528..b30666d48d2 100644
--- a/lib/private/files/cache/cache.php
+++ b/lib/private/files/cache/cache.php
@@ -145,6 +145,7 @@ class Cache implements ICache {
$data['size'] = 0 + $data['size'];
$data['mtime'] = (int)$data['mtime'];
$data['storage_mtime'] = (int)$data['storage_mtime'];
+ $data['encryptedVersion'] = (int)$data['encrypted'];
$data['encrypted'] = (bool)$data['encrypted'];
$data['storage'] = $this->storageId;
$data['mimetype'] = $this->mimetypeLoader->getMimetypeById($data['mimetype']);
@@ -345,8 +346,12 @@ class Cache implements ICache {
$queryParts[] = '`mtime`';
}
} elseif ($name === 'encrypted') {
- // Boolean to integer conversion
- $value = $value ? 1 : 0;
+ if(isset($data['encryptedVersion'])) {
+ $value = $data['encryptedVersion'];
+ } else {
+ // Boolean to integer conversion
+ $value = $value ? 1 : 0;
+ }
}
$params[] = $value;
$queryParts[] = '`' . $name . '`';
diff --git a/lib/private/files/fileinfo.php b/lib/private/files/fileinfo.php
index f22e1099e26..1d722a46735 100644
--- a/lib/private/files/fileinfo.php
+++ b/lib/private/files/fileinfo.php
@@ -194,6 +194,15 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
}
/**
+ * Return the currently version used for the HMAC in the encryption app
+ *
+ * @return int
+ */
+ public function getEncryptedVersion() {
+ return isset($this->data['encryptedVersion']) ? (int) $this->data['encryptedVersion'] : 1;
+ }
+
+ /**
* @return int
*/
public function getPermissions() {
diff --git a/lib/private/files/storage/wrapper/encryption.php b/lib/private/files/storage/wrapper/encryption.php
index f358bd59239..14d3b15bbae 100644
--- a/lib/private/files/storage/wrapper/encryption.php
+++ b/lib/private/files/storage/wrapper/encryption.php
@@ -39,6 +39,7 @@ use OCP\Encryption\Keys\IStorage;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Storage;
use OCP\ILogger;
+use OCP\Files\Cache\ICacheEntry;
class Encryption extends Wrapper {
@@ -129,13 +130,16 @@ class Encryption extends Wrapper {
if (isset($this->unencryptedSize[$fullPath])) {
$size = $this->unencryptedSize[$fullPath];
// update file cache
- if ($info) {
+ if ($info instanceof ICacheEntry) {
$info = $info->getData();
+ $info['encrypted'] = $info['encryptedVersion'];
} else {
- $info = [];
+ if (!is_array($info)) {
+ $info = [];
+ }
+ $info['encrypted'] = true;
}
- $info['encrypted'] = true;
$info['size'] = $size;
$this->getCache()->put($path, $info);
@@ -343,6 +347,7 @@ class Encryption extends Wrapper {
$shouldEncrypt = false;
$encryptionModule = null;
$header = $this->getHeader($path);
+ $signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
$fullPath = $this->getFullPath($path);
$encryptionModuleId = $this->util->getEncryptionModuleId($header);
@@ -377,7 +382,7 @@ class Encryption extends Wrapper {
|| $mode === 'wb'
|| $mode === 'wb+'
) {
- // don't overwrite encrypted files if encyption is not enabled
+ // don't overwrite encrypted files if encryption is not enabled
if ($targetIsEncrypted && $encryptionEnabled === false) {
throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
}
@@ -385,6 +390,7 @@ class Encryption extends Wrapper {
// if $encryptionModuleId is empty, the default module will be used
$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
+ $signed = true;
}
} else {
$info = $this->getCache()->get($path);
@@ -422,7 +428,7 @@ class Encryption extends Wrapper {
}
$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
- $size, $unencryptedSize, $headerSize);
+ $size, $unencryptedSize, $headerSize, $signed);
return $handle;
}
diff --git a/lib/private/files/stream/encryption.php b/lib/private/files/stream/encryption.php
index c884cd8fa07..63949035b5a 100644
--- a/lib/private/files/stream/encryption.php
+++ b/lib/private/files/stream/encryption.php
@@ -72,6 +72,9 @@ class Encryption extends Wrapper {
/** @var string */
protected $fullPath;
+ /** @var bool */
+ protected $signed;
+
/**
* header data returned by the encryption module, will be written to the file
* in case of a write operation
@@ -110,7 +113,8 @@ class Encryption extends Wrapper {
'size',
'unencryptedSize',
'encryptionStorage',
- 'headerSize'
+ 'headerSize',
+ 'signed'
);
}
@@ -132,6 +136,7 @@ class Encryption extends Wrapper {
* @param int $size
* @param int $unencryptedSize
* @param int $headerSize
+ * @param bool $signed
* @param string $wrapper stream wrapper class
* @return resource
*
@@ -148,6 +153,7 @@ class Encryption extends Wrapper {
$size,
$unencryptedSize,
$headerSize,
+ $signed,
$wrapper = 'OC\Files\Stream\Encryption') {
$context = stream_context_create(array(
@@ -164,7 +170,8 @@ class Encryption extends Wrapper {
'size' => $size,
'unencryptedSize' => $unencryptedSize,
'encryptionStorage' => $encStorage,
- 'headerSize' => $headerSize
+ 'headerSize' => $headerSize,
+ 'signed' => $signed
)
));
@@ -225,7 +232,7 @@ class Encryption extends Wrapper {
$this->position = 0;
$this->cache = '';
$this->writeFlag = false;
- $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize();
+ $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
if (
$mode === 'w'
@@ -392,8 +399,9 @@ class Encryption extends Wrapper {
}
public function stream_close() {
- $this->flush();
- $remainingData = $this->encryptionModule->end($this->fullPath);
+ $this->flush('end');
+ $position = (int)floor($this->position/$this->unencryptedBlockSize);
+ $remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end');
if ($this->readOnly === false) {
if(!empty($remainingData)) {
parent::stream_write($remainingData);
@@ -405,15 +413,17 @@ class Encryption extends Wrapper {
/**
* write block to file
+ * @param string $positionPrefix
*/
- protected function flush() {
+ protected function flush($positionPrefix = '') {
// write to disk only when writeFlag was set to 1
if ($this->writeFlag) {
// Disable the file proxies so that encryption is not
// automatically attempted when the file is written to disk -
// we are handling that separately here and we don't want to
// get into an infinite loop
- $encrypted = $this->encryptionModule->encrypt($this->cache);
+ $position = (int)floor($this->position/$this->unencryptedBlockSize);
+ $encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix);
$bytesWritten = parent::stream_write($encrypted);
$this->writeFlag = false;
// Check whether the write concerns the last block
@@ -440,7 +450,12 @@ class Encryption extends Wrapper {
if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) {
// Get the data from the file handle
$data = parent::stream_read($this->util->getBlockSize());
- $this->cache = $this->encryptionModule->decrypt($data);
+ $position = (int)floor($this->position/$this->unencryptedBlockSize);
+ $numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize);
+ if($numberOfChunks === $position) {
+ $position .= 'end';
+ }
+ $this->cache = $this->encryptionModule->decrypt($data, $position);
}
}
diff --git a/lib/public/encryption/iencryptionmodule.php b/lib/public/encryption/iencryptionmodule.php
index 426e4ddecce..45e0b79c2a9 100644
--- a/lib/public/encryption/iencryptionmodule.php
+++ b/lib/public/encryption/iencryptionmodule.php
@@ -119,10 +119,11 @@ interface IEncryptionModule {
* get size of the unencrypted payload per block.
* ownCloud read/write files with a block size of 8192 byte
*
- * @return integer
- * @since 8.1.0
+ * @param bool $signed
+ * @return int
+ * @since 8.1.0 optional parameter $signed was added in 9.0.0
*/
- public function getUnencryptedBlockSize();
+ public function getUnencryptedBlockSize($signed = false);
/**
* check if the encryption module is able to read the file,
diff --git a/settings/changepassword/controller.php b/settings/changepassword/controller.php
index bfa599c1d04..8469ec1423a 100644
--- a/settings/changepassword/controller.php
+++ b/settings/changepassword/controller.php
@@ -89,7 +89,8 @@ class Controller {
$crypt = new \OCA\Encryption\Crypto\Crypt(
\OC::$server->getLogger(),
\OC::$server->getUserSession(),
- \OC::$server->getConfig());
+ \OC::$server->getConfig(),
+ \OC::$server->getL10N('encryption'));
$keyStorage = \OC::$server->getEncryptionKeyStorage();
$util = new \OCA\Encryption\Util(
new \OC\Files\View(),
diff --git a/tests/lib/files/stream/encryption.php b/tests/lib/files/stream/encryption.php
index f9d8f076b63..f67dd09bc4d 100644
--- a/tests/lib/files/stream/encryption.php
+++ b/tests/lib/files/stream/encryption.php
@@ -117,6 +117,7 @@ class Encryption extends \Test\TestCase {
$header->setAccessible(true);
$header->setValue($streamWrapper, array());
$header->setAccessible(false);
+ $this->invokePrivate($streamWrapper, 'signed', [true]);
// call stream_open, that's the method we want to test
$dummyVar = 'foo';