Merge pull request #21557 from owncloud/use-hmac-over-encryption

Add integrity protection using Encrypt-Then-MAC to default encryption module
This commit is contained in:
Lukas Reschke 2016-02-09 23:45:27 +01:00
commit 53d57bffed
18 changed files with 499 additions and 137 deletions

View File

@ -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',

View File

@ -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));

View File

@ -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'],
@ -405,42 +463,103 @@ class Crypt {
$cipher);
}
/**
* check for valid signature
*
* @param string $data
* @param string $passPhrase
* @param string $expectedSignature
* @throws HintException
*/
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'));
}
}
/**
* create signature
*
* @param string $data
* @param string $passPhrase
* @return string
*/
private function createSignature($data, $passPhrase) {
$passPhrase = hash('sha512', $passPhrase . 'a', true);
$signature = hash_hmac('sha256', $data, $passPhrase);
return $signature;
}
/**
* remove padding
*
* @param $padded
* @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) {
if (substr($padded, -2) === 'xx') {
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;
}
/**
* split iv from encrypted content
* 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|false $catFile
* @return string
* @param string $catFile
* @param string $cipher
* @return array
*/
private function splitIv($catFile) {
// Fetch encryption metadata from end of file
$meta = substr($catFile, -22);
// Fetch IV from end of file
$iv = substr($meta, -16);
// Remove IV and IV Identifier text to expose encrypted content
$encrypted = substr($catFile, 0, -22);
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
@ -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);
}
/**

View File

@ -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;
@ -55,6 +56,9 @@ class Encryption implements IEncryptionModule {
/** @var string */
private $path;
/** @var string */
private $realPath;
/** @var string */
private $user;
@ -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;
}
/**

View File

@ -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;
@ -412,6 +414,37 @@ class KeyManager {
return '';
}
/**
* 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
*
@ -546,6 +579,7 @@ class KeyManager {
/**
* @param string $path
* @return bool
*/
public function deleteAllFileKeys($path) {
return $this->keyStorage->deleteAllFileKeys($path);

View File

@ -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(),

View File

@ -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(),

View File

@ -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);
}
}

View File

@ -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'],

View File

@ -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'),

View File

@ -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();
}
}

View File

@ -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 . '`';

View File

@ -193,6 +193,15 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
return $this->data['encrypted'];
}
/**
* 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
*/

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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(),

View File

@ -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';