diff options
author | Vincent Petry <pvince81@owncloud.com> | 2014-08-13 12:34:21 +0200 |
---|---|---|
committer | Vincent Petry <pvince81@owncloud.com> | 2014-08-13 12:34:21 +0200 |
commit | f282a5cff00d2e7ecbfaa0d93d7ab0bf30921701 (patch) | |
tree | e5d23e0cac3374fac043f04151cc4dabe33920eb /apps | |
parent | fc46fbd1541bda8eb094bd8ee64827fa1cbf1fd0 (diff) | |
parent | ffa6b330477193dd5f438980bd2736555aa738e6 (diff) | |
download | nextcloud-server-f282a5cff00d2e7ecbfaa0d93d7ab0bf30921701.tar.gz nextcloud-server-f282a5cff00d2e7ecbfaa0d93d7ab0bf30921701.zip |
Merge pull request #9754 from owncloud/enc_support_aes_256
[encryption] support aes 256
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files_encryption/ajax/changeRecoveryPassword.php | 11 | ||||
-rw-r--r-- | apps/files_encryption/ajax/updatePrivateKeyPassword.php | 14 | ||||
-rw-r--r-- | apps/files_encryption/hooks/hooks.php | 29 | ||||
-rwxr-xr-x | apps/files_encryption/lib/crypt.php | 131 | ||||
-rw-r--r-- | apps/files_encryption/lib/exceptions.php | 9 | ||||
-rwxr-xr-x | apps/files_encryption/lib/helper.php | 39 | ||||
-rwxr-xr-x | apps/files_encryption/lib/keymanager.php | 37 | ||||
-rw-r--r-- | apps/files_encryption/lib/session.php | 12 | ||||
-rw-r--r-- | apps/files_encryption/lib/stream.php | 91 | ||||
-rw-r--r-- | apps/files_encryption/lib/util.php | 45 | ||||
-rwxr-xr-x | apps/files_encryption/tests/crypt.php | 203 | ||||
-rw-r--r-- | apps/files_encryption/tests/keymanager.php | 36 | ||||
-rwxr-xr-x | apps/files_encryption/tests/share.php | 6 | ||||
-rwxr-xr-x | apps/files_encryption/tests/util.php | 2 |
14 files changed, 498 insertions, 167 deletions
diff --git a/apps/files_encryption/ajax/changeRecoveryPassword.php b/apps/files_encryption/ajax/changeRecoveryPassword.php index 0cb010d3b56..99cc7b3cdde 100644 --- a/apps/files_encryption/ajax/changeRecoveryPassword.php +++ b/apps/files_encryption/ajax/changeRecoveryPassword.php @@ -35,11 +35,12 @@ $encryptedRecoveryKey = $view->file_get_contents($keyPath); $decryptedRecoveryKey = \OCA\Encryption\Crypt::decryptPrivateKey($encryptedRecoveryKey, $oldPassword); if ($decryptedRecoveryKey) { - - $encryptedRecoveryKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($decryptedRecoveryKey, $newPassword); - $view->file_put_contents($keyPath, $encryptedRecoveryKey); - - $return = true; + $cipher = \OCA\Encryption\Helper::getCipher(); + $encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($decryptedRecoveryKey, $newPassword, $cipher); + if ($encryptedKey) { + \OCA\Encryption\Keymanager::setPrivateSystemKey($encryptedKey, $keyId . '.private.key'); + $return = true; + } } \OC_FileProxy::$enabled = $proxyStatus; diff --git a/apps/files_encryption/ajax/updatePrivateKeyPassword.php b/apps/files_encryption/ajax/updatePrivateKeyPassword.php index f7d20c486cf..a14c9fe5076 100644 --- a/apps/files_encryption/ajax/updatePrivateKeyPassword.php +++ b/apps/files_encryption/ajax/updatePrivateKeyPassword.php @@ -35,13 +35,13 @@ $encryptedKey = $view->file_get_contents($keyPath); $decryptedKey = \OCA\Encryption\Crypt::decryptPrivateKey($encryptedKey, $oldPassword); if ($decryptedKey) { - - $encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($decryptedKey, $newPassword); - $view->file_put_contents($keyPath, $encryptedKey); - - $session->setPrivateKey($decryptedKey); - - $return = true; + $cipher = \OCA\Encryption\Helper::getCipher(); + $encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($decryptedKey, $newPassword, $cipher); + if ($encryptedKey) { + \OCA\Encryption\Keymanager::setPrivateKey($encryptedKey, $user); + $session->setPrivateKey($decryptedKey); + $return = true; + } } \OC_FileProxy::$enabled = $proxyStatus; diff --git a/apps/files_encryption/hooks/hooks.php b/apps/files_encryption/hooks/hooks.php index bd2268aa048..b1e7e8c52a5 100644 --- a/apps/files_encryption/hooks/hooks.php +++ b/apps/files_encryption/hooks/hooks.php @@ -200,10 +200,14 @@ class Hooks { $privateKey = $session->getPrivateKey();
// Encrypt private key with new user pwd as passphrase
- $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password']);
+ $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password'], Helper::getCipher());
// Save private key
- Keymanager::setPrivateKey($encryptedPrivateKey);
+ if ($encryptedPrivateKey) {
+ Keymanager::setPrivateKey($encryptedPrivateKey, \OCP\User::getUser());
+ } else {
+ \OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR);
+ }
// NOTE: Session does not need to be updated as the
// private key has not changed, only the passphrase
@@ -238,16 +242,17 @@ class Hooks { // Save public key
$view->file_put_contents('/public-keys/' . $user . '.public.key', $keypair['publicKey']);
- // Encrypt private key empty passphrase
- $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword);
-
- // Save private key
- $view->file_put_contents(
- '/' . $user . '/files_encryption/' . $user . '.private.key', $encryptedPrivateKey);
-
- if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
- $util = new Util($view, $user);
- $util->recoverUsersFiles($recoveryPassword);
+ // Encrypt private key with new password
+ $encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword, Helper::getCipher());
+ if ($encryptedKey) {
+ Keymanager::setPrivateKey($encryptedKey, $user);
+
+ if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
+ $util = new Util($view, $user);
+ $util->recoverUsersFiles($recoveryPassword);
+ }
+ } else {
+ \OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR);
}
\OC_FileProxy::$enabled = $proxyStatus;
diff --git a/apps/files_encryption/lib/crypt.php b/apps/files_encryption/lib/crypt.php index 18f0224391d..7974598729e 100755 --- a/apps/files_encryption/lib/crypt.php +++ b/apps/files_encryption/lib/crypt.php @@ -36,6 +36,11 @@ class Crypt { const ENCRYPTION_PRIVATE_KEY_NOT_VALID_ERROR = 2;
const ENCRYPTION_NO_SHARE_KEY_FOUND = 3;
+ const BLOCKSIZE = 8192; // block size will always be 8192 for a PHP stream https://bugs.php.net/bug.php?id=21641
+ const DEFAULT_CIPHER = 'AES-256-CFB';
+
+ const HEADERSTART = 'HBEGIN';
+ const HEADEREND = 'HEND';
/**
* return encryption mode client or server side encryption
@@ -181,19 +186,22 @@ class Crypt { * @param string $plainContent
* @param string $iv
* @param string $passphrase
+ * @param string $cypher used for encryption, currently we support AES-128-CFB and AES-256-CFB
* @return string encrypted file content
+ * @throws \OCA\Encryption\Exceptions\EncryptionException
*/
- private static function encrypt($plainContent, $iv, $passphrase = '') {
+ private static function encrypt($plainContent, $iv, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
- if ($encryptedContent = openssl_encrypt($plainContent, 'AES-128-CFB', $passphrase, false, $iv)) {
- return $encryptedContent;
- } else {
- \OCP\Util::writeLog('Encryption library', 'Encryption (symmetric) of content failed', \OCP\Util::ERROR);
- \OCP\Util::writeLog('Encryption library', openssl_error_string(), \OCP\Util::ERROR);
- return false;
+ $encryptedContent = openssl_encrypt($plainContent, $cipher, $passphrase, false, $iv);
+ if (!$encryptedContent) {
+ $error = "Encryption (symmetric) of content failed: " . openssl_error_string();
+ \OCP\Util::writeLog('Encryption library', $error, \OCP\Util::ERROR);
+ throw new Exceptions\EncryptionException($error, 50);
}
+ return $encryptedContent;
+
}
/**
@@ -201,19 +209,18 @@ class Crypt { * @param string $encryptedContent
* @param string $iv
* @param string $passphrase
+ * @param string $cipher cipher user for decryption, currently we support aes128 and aes256
* @throws \Exception
* @return string decrypted file content
*/
- private static function decrypt($encryptedContent, $iv, $passphrase) {
+ private static function decrypt($encryptedContent, $iv, $passphrase, $cipher = Crypt::DEFAULT_CIPHER) {
- if ($plainContent = openssl_decrypt($encryptedContent, 'AES-128-CFB', $passphrase, false, $iv)) {
+ $plainContent = openssl_decrypt($encryptedContent, $cipher, $passphrase, false, $iv);
+ if ($plainContent) {
return $plainContent;
-
} else {
-
throw new \Exception('Encryption library: Decryption (symmetric) of content failed');
-
}
}
@@ -261,11 +268,12 @@ class Crypt { * Symmetrically encrypts a string and returns keyfile content
* @param string $plainContent content to be encrypted in keyfile
* @param string $passphrase
+ * @param string $cypher used for encryption, currently we support AES-128-CFB and AES-256-CFB
* @return false|string encrypted content combined with IV
* @note IV need not be specified, as it will be stored in the returned keyfile
* and remain accessible therein.
*/
- public static function symmetricEncryptFileContent($plainContent, $passphrase = '') {
+ public static function symmetricEncryptFileContent($plainContent, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
if (!$plainContent) {
\OCP\Util::writeLog('Encryption library', 'symmetrically encryption failed, no content given.', \OCP\Util::ERROR);
@@ -274,15 +282,16 @@ class Crypt { $iv = self::generateIv();
- if ($encryptedContent = self::encrypt($plainContent, $iv, $passphrase)) {
+ try {
+ $encryptedContent = self::encrypt($plainContent, $iv, $passphrase, $cipher);
// Combine content to encrypt with IV identifier and actual IV
$catfile = self::concatIv($encryptedContent, $iv);
$padded = self::addPadding($catfile);
return $padded;
-
- } else {
- \OCP\Util::writeLog('Encryption library', 'Encryption (symmetric) of keyfile content failed', \OCP\Util::ERROR);
+ } catch (OCA\Encryption\Exceptions\EncryptionException $e) {
+ $message = 'Could not encrypt file content (code: ' . $e->getCode . '): ';
+ \OCP\Util::writeLog('files_encryption', $message . $e->getMessage, \OCP\Util::ERROR);
return false;
}
@@ -293,6 +302,7 @@ class Crypt { * Symmetrically decrypts keyfile content
* @param string $keyfileContent
* @param string $passphrase
+ * @param string $cipher cipher used for decryption, currently aes128 and aes256 is supported.
* @throws \Exception
* @return string|false
* @internal param string $source
@@ -302,7 +312,7 @@ class Crypt { *
* This function decrypts a file
*/
- public static function symmetricDecryptFileContent($keyfileContent, $passphrase = '') {
+ public static function symmetricDecryptFileContent($keyfileContent, $passphrase = '', $cipher = Crypt::DEFAULT_CIPHER) {
if (!$keyfileContent) {
@@ -316,7 +326,7 @@ class Crypt { // Split into enc data and catfile
$catfile = self::splitIv($noPadding);
- if ($plainContent = self::decrypt($catfile['encrypted'], $catfile['iv'], $passphrase)) {
+ if ($plainContent = self::decrypt($catfile['encrypted'], $catfile['iv'], $passphrase, $cipher)) {
return $plainContent;
@@ -328,6 +338,7 @@ class Crypt { /**
* Decrypt private key and check if the result is a valid keyfile
+ *
* @param string $encryptedKey encrypted keyfile
* @param string $passphrase to decrypt keyfile
* @return string|false encrypted private key or false
@@ -336,7 +347,15 @@ class Crypt { */
public static function decryptPrivateKey($encryptedKey, $passphrase) {
- $plainKey = self::symmetricDecryptFileContent($encryptedKey, $passphrase);
+ $header = self::parseHeader($encryptedKey);
+ $cipher = self::getCipher($header);
+
+ // if we found a header we need to remove it from the key we want to decrypt
+ if (!empty($header)) {
+ $encryptedKey = substr($encryptedKey, strpos($encryptedKey, self::HEADEREND) + strlen(self::HEADEREND));
+ }
+
+ $plainKey = self::symmetricDecryptFileContent($encryptedKey, $passphrase, $cipher);
// check if this a valid private key
$res = openssl_pkey_get_private($plainKey);
@@ -481,4 +500,76 @@ class Crypt { }
+ /**
+ * read header into array
+ *
+ * @param string $data
+ * @return array
+ */
+ public static function parseHeader($data) {
+
+ $result = array();
+
+ if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) {
+ $endAt = strpos($data, self::HEADEREND);
+ $header = substr($data, 0, $endAt + strlen(self::HEADEREND));
+
+ // +1 to not start with an ':' which would result in empty element at the beginning
+ $exploded = explode(':', substr($header, strlen(self::HEADERSTART)+1));
+
+ $element = array_shift($exploded);
+ while ($element !== self::HEADEREND) {
+
+ $result[$element] = array_shift($exploded);
+
+ $element = array_shift($exploded);
+
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * check if data block is the header
+ *
+ * @param string $data
+ * @return boolean
+ */
+ public static function isHeader($data) {
+
+ if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * get chiper from header
+ *
+ * @param array $header
+ * @throws \OCA\Encryption\Exceptions\EncryptionException
+ */
+ public static function getCipher($header) {
+ $cipher = isset($header['cipher']) ? $header['cipher'] : 'AES-128-CFB';
+
+ if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') {
+
+ throw new \OCA\Encryption\Exceptions\EncryptionException('file header broken, no supported cipher defined', 40);
+ }
+
+ return $cipher;
+ }
+
+ /**
+ * generate header for encrypted file
+ */
+ public static function generateHeader() {
+ $cipher = Helper::getCipher();
+ $header = self::HEADERSTART . ':cipher:' . $cipher . ':' . self::HEADEREND;
+
+ return $header;
+ }
+
}
diff --git a/apps/files_encryption/lib/exceptions.php b/apps/files_encryption/lib/exceptions.php index a409b0f0fb2..3ea27faf406 100644 --- a/apps/files_encryption/lib/exceptions.php +++ b/apps/files_encryption/lib/exceptions.php @@ -22,6 +22,15 @@ namespace OCA\Encryption\Exceptions; +/** + * General encryption exception + * Possible Error Codes: + * 10 - unexpected end of encryption header + * 20 - unexpected blog size + * 30 - encryption header to large + * 40 - unknown cipher + * 50 - encryption failed + */ class EncryptionException extends \Exception { } diff --git a/apps/files_encryption/lib/helper.php b/apps/files_encryption/lib/helper.php index 214e212b675..d427c51732f 100755 --- a/apps/files_encryption/lib/helper.php +++ b/apps/files_encryption/lib/helper.php @@ -144,19 +144,17 @@ class Helper { $view->file_put_contents('/public-keys/' . $recoveryKeyId . '.public.key', $keypair['publicKey']); - // Encrypt private key empty passphrase - $encryptedPrivateKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $recoveryPassword); - - // Save private key - $view->file_put_contents('/owncloud_private_key/' . $recoveryKeyId . '.private.key', $encryptedPrivateKey); + $cipher = \OCA\Encryption\Helper::getCipher(); + $encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $recoveryPassword, $cipher); + if ($encryptedKey) { + Keymanager::setPrivateSystemKey($encryptedKey, $recoveryKeyId . '.private.key'); + // Set recoveryAdmin as enabled + $appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1); + $return = true; + } \OC_FileProxy::$enabled = true; - // Set recoveryAdmin as enabled - $appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1); - - $return = true; - } else { // get recovery key and check the password $util = new \OCA\Encryption\Util(new \OC\Files\View('/'), \OCP\User::getUser()); $return = $util->checkRecoveryPassword($recoveryPassword); @@ -230,7 +228,6 @@ class Helper { return $return; } - /** * checks if access is public/anonymous user * @return bool @@ -478,5 +475,25 @@ class Helper { return false; } + + /** + * read the cipher used for encryption from the config.php + * + * @return string + */ + public static function getCipher() { + + $cipher = \OCP\Config::getSystemValue('cipher', Crypt::DEFAULT_CIPHER); + + if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') { + \OCP\Util::writeLog('files_encryption', + 'wrong cipher defined in config.php, only AES-128-CFB and AES-256-CFB is supported. Fall back ' . Crypt::DEFAULT_CIPHER, + \OCP\Util::WARN); + + $cipher = Crypt::DEFAULT_CIPHER; + } + + return $cipher; + } } diff --git a/apps/files_encryption/lib/keymanager.php b/apps/files_encryption/lib/keymanager.php index da84e975a05..931469f4b74 100755 --- a/apps/files_encryption/lib/keymanager.php +++ b/apps/files_encryption/lib/keymanager.php @@ -258,9 +258,13 @@ class Keymanager { * @note Encryption of the private key must be performed by client code * as no encryption takes place here */ - public static function setPrivateKey($key) { + public static function setPrivateKey($key, $user = '') { - $user = \OCP\User::getUser(); + if ($user === '') { + $user = \OCP\User::getUser(); + } + + $header = Crypt::generateHeader(); $view = new \OC\Files\View('/' . $user . '/files_encryption'); @@ -271,7 +275,7 @@ class Keymanager { $view->mkdir(''); } - $result = $view->file_put_contents($user . '.private.key', $key); + $result = $view->file_put_contents($user . '.private.key', $header . $key); \OC_FileProxy::$enabled = $proxyStatus; @@ -280,6 +284,33 @@ class Keymanager { } /** + * write private system key (recovery and public share key) to disk + * + * @param string $key encrypted key + * @param string $keyName name of the key file + * @return boolean + */ + public static function setPrivateSystemKey($key, $keyName) { + + $header = Crypt::generateHeader(); + + $view = new \OC\Files\View('/owncloud_private_key'); + + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + if (!$view->file_exists('')) { + $view->mkdir(''); + } + + $result = $view->file_put_contents($keyName, $header . $key); + + \OC_FileProxy::$enabled = $proxyStatus; + + return $result; + } + + /** * store share key * * @param \OC\Files\View $view diff --git a/apps/files_encryption/lib/session.php b/apps/files_encryption/lib/session.php index 84599181ba2..ff8fbd24ecb 100644 --- a/apps/files_encryption/lib/session.php +++ b/apps/files_encryption/lib/session.php @@ -80,11 +80,13 @@ class Session { $this->view->file_put_contents('/public-keys/' . $publicShareKeyId . '.public.key', $keypair['publicKey']); // Encrypt private key empty passphrase - $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], ''); - - // Save private key - $this->view->file_put_contents( - '/owncloud_private_key/' . $publicShareKeyId . '.private.key', $encryptedPrivateKey); + $cipher = \OCA\Encryption\Helper::getCipher(); + $encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], '', $cipher); + if ($encryptedKey) { + Keymanager::setPrivateSystemKey($encryptedKey, $publicShareKeyId . '.private.key'); + } else { + \OCP\Util::writeLog('files_encryption', 'Could not create public share keys', \OCP\Util::ERROR); + } \OC_FileProxy::$enabled = $proxyStatus; diff --git a/apps/files_encryption/lib/stream.php b/apps/files_encryption/lib/stream.php index 341114214d5..f74812a7253 100644 --- a/apps/files_encryption/lib/stream.php +++ b/apps/files_encryption/lib/stream.php @@ -2,9 +2,10 @@ /** * ownCloud * - * @author Robin Appelman - * @copyright 2012 Sam Tuke <samtuke@owncloud.com>, 2011 Robin Appelman - * <icewind1991@gmail.com> + * @author Bjoern Schiessle, Robin Appelman + * @copyright 2014 Bjoern Schiessle <schiessle@owncloud.com> + * 2012 Sam Tuke <samtuke@owncloud.com>, + * 2011 Robin Appelman <icewind1991@gmail.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -49,9 +50,11 @@ namespace OCA\Encryption; * encryption proxies are used and keyfiles deleted. */ class Stream { + + const PADDING_CHAR = '-'; + private $plainKey; private $encKeyfiles; - private $rawPath; // The raw path relative to the data dir private $relPath; // rel path to users file dir private $userId; @@ -66,6 +69,9 @@ class Stream { private $newFile; // helper var, we only need to write the keyfile for new files private $isLocalTmpFile = false; // do we operate on a local tmp file private $localTmpFile; // path of local tmp file + private $headerWritten = false; + private $containHeader = false; // the file contain a header + private $cipher; // cipher used for encryption/decryption /** * @var \OC\Files\View @@ -87,6 +93,9 @@ class Stream { */ public function stream_open($path, $mode, $options, &$opened_path) { + // read default cipher from config + $this->cipher = Helper::getCipher(); + // assume that the file already exist before we decide it finally in getKey() $this->newFile = false; @@ -150,6 +159,9 @@ class Stream { } $this->size = $this->rootView->filesize($this->rawPath); + + $this->readHeader(); + } if ($this->isLocalTmpFile) { @@ -178,6 +190,29 @@ class Stream { } + private function readHeader() { + + if ($this->isLocalTmpFile) { + $handle = fopen($this->localTmpFile, 'r'); + } else { + $handle = $this->rootView->fopen($this->rawPath, 'r'); + } + + if (is_resource($handle)) { + $data = fread($handle, Crypt::BLOCKSIZE); + + $header = Crypt::parseHeader($data); + $this->cipher = Crypt::getCipher($header); + + // remeber that we found a header + if (!empty($header)) { + $this->containHeader = true; + } + + fclose($handle); + } + } + /** * Returns the current position of the file pointer * @return int position of the file pointer @@ -195,6 +230,11 @@ class Stream { $this->flush(); + // ignore the header and just overstep it + if ($this->containHeader) { + $offset += Crypt::BLOCKSIZE; + } + // this wrapper needs to return "true" for success. // the fseek call itself returns 0 on succeess return !fseek($this->handle, $offset, $whence); @@ -204,25 +244,25 @@ class Stream { /** * @param int $count * @return bool|string - * @throws \Exception + * @throws \OCA\Encryption\Exceptions\EncryptionException */ public function stream_read($count) { $this->writeCache = ''; - if ($count !== 8192) { - - // $count will always be 8192 https://bugs.php.net/bug.php?id=21641 - // This makes this function a lot simpler, but will break this class if the above 'bug' gets 'fixed' + if ($count !== Crypt::BLOCKSIZE) { \OCP\Util::writeLog('Encryption library', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', \OCP\Util::FATAL); - - die(); - + throw new \OCA\Encryption\Exceptions\EncryptionException('expected a blog size of 8192 byte', 20); } // Get the data from the file handle $data = fread($this->handle, $count); + // if this block contained the header we move on to the next block + if (Crypt::isHeader($data)) { + $data = fread($this->handle, $count); + } + $result = null; if (strlen($data)) { @@ -236,7 +276,7 @@ class Stream { } else { // Decrypt data - $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey); + $result = Crypt::symmetricDecryptFileContent($data, $this->plainKey, $this->cipher); } } @@ -254,7 +294,7 @@ class Stream { public function preWriteEncrypt($plainData, $key) { // Encrypt data to 'catfile', which includes IV - if ($encrypted = Crypt::symmetricEncryptFileContent($plainData, $key)) { + if ($encrypted = Crypt::symmetricEncryptFileContent($plainData, $key, $this->cipher)) { return $encrypted; @@ -318,6 +358,25 @@ class Stream { } /** + * write header at beginning of encrypted file + * + * @throws Exceptions\EncryptionException + */ + private function writeHeader() { + + $header = Crypt::generateHeader(); + + if (strlen($header) > Crypt::BLOCKSIZE) { + throw new Exceptions\EncryptionException('max header size exceeded', 30); + } + + $paddedHeader = str_pad($header, Crypt::BLOCKSIZE, self::PADDING_CHAR, STR_PAD_RIGHT); + + fwrite($this->handle, $paddedHeader); + $this->headerWritten = true; + } + + /** * Handle plain data from the stream, and write it in 8192 byte blocks * @param string $data data to be written to disk * @note the data will be written to the path stored in the stream handle, set in stream_open() @@ -334,6 +393,10 @@ class Stream { return strlen($data); } + if ($this->headerWritten === false) { + $this->writeHeader(); + } + // 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 diff --git a/apps/files_encryption/lib/util.php b/apps/files_encryption/lib/util.php index 3786a465411..087dada7f1b 100644 --- a/apps/files_encryption/lib/util.php +++ b/apps/files_encryption/lib/util.php @@ -167,11 +167,12 @@ class Util { \OC_FileProxy::$enabled = false; // Encrypt private key with user pwd as passphrase - $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $passphrase); + $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $passphrase, Helper::getCipher()); // Save key-pair if ($encryptedPrivateKey) { - $this->view->file_put_contents($this->privateKeyPath, $encryptedPrivateKey); + $header = crypt::generateHeader(); + $this->view->file_put_contents($this->privateKeyPath, $header . $encryptedPrivateKey); $this->view->file_put_contents($this->publicKeyPath, $keypair['publicKey']); } @@ -384,8 +385,14 @@ class Util { && $this->isEncryptedPath($path) ) { - // get the size from filesystem - $size = $this->view->filesize($path); + $offset = 0; + if ($this->containHeader($path)) { + $offset = Crypt::BLOCKSIZE; + } + + // get the size from filesystem if the file contains a encryption header we + // we substract it + $size = $this->view->filesize($path) - $offset; // fast path, else the calculation for $lastChunkNr is bogus if ($size === 0) { @@ -396,15 +403,15 @@ class Util { // calculate last chunk nr // next highest is end of chunks, one subtracted is last one // we have to read the last chunk, we can't just calculate it (because of padding etc) - $lastChunkNr = ceil($size/ 8192) - 1; - $lastChunkSize = $size - ($lastChunkNr * 8192); + $lastChunkNr = ceil($size/ Crypt::BLOCKSIZE) - 1; + $lastChunkSize = $size - ($lastChunkNr * Crypt::BLOCKSIZE); // open stream $stream = fopen('crypt://' . $path, "r"); if (is_resource($stream)) { // calculate last chunk position - $lastChunckPos = ($lastChunkNr * 8192); + $lastChunckPos = ($lastChunkNr * Crypt::BLOCKSIZE); // seek to end if (@fseek($stream, $lastChunckPos) === -1) { @@ -439,6 +446,30 @@ class Util { } /** + * check if encrypted file contain a encryption header + * + * @param string $path + * @return boolean + */ + private function containHeader($path) { + // Disable encryption proxy to read the raw data + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + $isHeader = false; + $handle = $this->view->fopen($path, 'r'); + + if (is_resource($handle)) { + $firstBlock = fread($handle, Crypt::BLOCKSIZE); + $isHeader = Crypt::isHeader($firstBlock); + } + + \OC_FileProxy::$enabled = $proxyStatus; + + return $isHeader; + } + + /** * fix the file size of the encrypted file * @param string $path absolute path * @return boolean true / false if file is encrypted diff --git a/apps/files_encryption/tests/crypt.php b/apps/files_encryption/tests/crypt.php index a1a51c749b0..5eb9580e3b4 100755 --- a/apps/files_encryption/tests/crypt.php +++ b/apps/files_encryption/tests/crypt.php @@ -96,6 +96,7 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { } $this->assertTrue(\OC_FileProxy::$enabled); + \OCP\Config::deleteSystemValue('cipher'); } public static function tearDownAfterClass() { @@ -121,7 +122,9 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { // test successful decrypt $crypted = Encryption\Crypt::symmetricEncryptFileContent($this->genPrivateKey, 'hat'); - $decrypted = Encryption\Crypt::decryptPrivateKey($crypted, 'hat'); + $header = Encryption\Crypt::generateHeader(); + + $decrypted = Encryption\Crypt::decryptPrivateKey($header . $crypted, 'hat'); $this->assertEquals($this->genPrivateKey, $decrypted); @@ -154,12 +157,28 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { /** * @medium */ + function testSymmetricEncryptFileContentAes128() { + + # TODO: search in keyfile for actual content as IV will ensure this test always passes + + $crypted = Encryption\Crypt::symmetricEncryptFileContent($this->dataShort, 'hat', 'AES-128-CFB'); + + $this->assertNotEquals($this->dataShort, $crypted); + + + $decrypt = Encryption\Crypt::symmetricDecryptFileContent($crypted, 'hat', 'AES-128-CFB'); + + $this->assertEquals($this->dataShort, $decrypt); + + } + + /** + * @medium + */ function testSymmetricStreamEncryptShortFileContent() { $filename = 'tmp-' . uniqid() . '.test'; - $util = new Encryption\Util(new \OC\Files\View(), $this->userId); - $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/'. $filename, $this->dataShort); // Test that data was successfully written @@ -178,26 +197,52 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { // Check that the file was encrypted before being written to disk $this->assertNotEquals($this->dataShort, $retreivedCryptedFile); - // Get the encrypted keyfile - $encKeyfile = Encryption\Keymanager::getFileKey($this->view, $util, $filename); + // Get file contents with the encryption wrapper + $decrypted = file_get_contents('crypt:///' . $this->userId . '/files/'. $filename); + + // Check that decrypted data matches + $this->assertEquals($this->dataShort, $decrypted); + + // Teardown + $this->view->unlink($this->userId . '/files/' . $filename); + + Encryption\Keymanager::deleteFileKey($this->view, $filename); + } + + /** + * @medium + */ + function testSymmetricStreamEncryptShortFileContentAes128() { + + $filename = 'tmp-' . uniqid() . '.test'; + + \OCP\Config::setSystemValue('cipher', 'AES-128-CFB'); + + $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/'. $filename, $this->dataShort); - // Attempt to fetch the user's shareKey - $shareKey = Encryption\Keymanager::getShareKey($this->view, $this->userId, $util, $filename); + // Test that data was successfully written + $this->assertTrue(is_int($cryptedFile)); - // get session - $session = new \OCA\Encryption\Session($this->view); + \OCP\Config::deleteSystemValue('cipher'); - // get private key - $privateKey = $session->getPrivateKey($this->userId); + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Get file contents without using any wrapper to get it's actual contents on disk + $retreivedCryptedFile = $this->view->file_get_contents($this->userId . '/files/' . $filename); + + // Re-enable proxy - our work is done + \OC_FileProxy::$enabled = $proxyStatus; - // Decrypt keyfile with shareKey - $plainKeyfile = Encryption\Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); + // Check that the file was encrypted before being written to disk + $this->assertNotEquals($this->dataShort, $retreivedCryptedFile); - // Manually decrypt - $manualDecrypt = Encryption\Crypt::symmetricDecryptFileContent($retreivedCryptedFile, $plainKeyfile); + // Get file contents with the encryption wrapper + $decrypted = file_get_contents('crypt:///' . $this->userId . '/files/'. $filename); // Check that decrypted data matches - $this->assertEquals($this->dataShort, $manualDecrypt); + $this->assertEquals($this->dataShort, $decrypted); // Teardown $this->view->unlink($this->userId . '/files/' . $filename); @@ -217,8 +262,6 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { // Generate a a random filename $filename = 'tmp-' . uniqid() . '.test'; - $util = new Encryption\Util(new \OC\Files\View(), $this->userId); - // Save long data as encrypted file using stream wrapper $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/' . $filename, $this->dataLong . $this->dataLong); @@ -239,50 +282,57 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { // Check that the file was encrypted before being written to disk $this->assertNotEquals($this->dataLong . $this->dataLong, $retreivedCryptedFile); - // Manuallly split saved file into separate IVs and encrypted chunks - $r = preg_split('/(00iv00.{16,18})/', $retreivedCryptedFile, NULL, PREG_SPLIT_DELIM_CAPTURE); + $decrypted = file_get_contents('crypt:///' . $this->userId . '/files/'. $filename); - //print_r($r); + $this->assertEquals($this->dataLong . $this->dataLong, $decrypted); - // Join IVs and their respective data chunks - $e = array(); - $i = 0; - while ($i < count($r)-1) { - $e[] = $r[$i] . $r[$i+1]; - $i = $i + 2; - } + // Teardown - //print_r($e); + $this->view->unlink($this->userId . '/files/' . $filename); - // Get the encrypted keyfile - $encKeyfile = Encryption\Keymanager::getFileKey($this->view, $util, $filename); + Encryption\Keymanager::deleteFileKey($this->view, $filename); - // Attempt to fetch the user's shareKey - $shareKey = Encryption\Keymanager::getShareKey($this->view, $this->userId, $util, $filename); + } - // get session - $session = new \OCA\Encryption\Session($this->view); + /** + * @medium + * Test that data that is written by the crypto stream wrapper with AES 128 + * @note Encrypted data is manually prepared and decrypted here to avoid dependency on success of stream_read + * @note If this test fails with truncate content, check that enough array slices are being rejoined to form $e, as the crypt.php file may have gotten longer and broken the manual + * reassembly of its data + */ + function testSymmetricStreamEncryptLongFileContentAes128() { - // get private key - $privateKey = $session->getPrivateKey($this->userId); + // Generate a a random filename + $filename = 'tmp-' . uniqid() . '.test'; - // Decrypt keyfile with shareKey - $plainKeyfile = Encryption\Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); + \OCP\Config::setSystemValue('cipher', 'AES-128-CFB'); - // Set var for reassembling decrypted content - $decrypt = ''; + // Save long data as encrypted file using stream wrapper + $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/' . $filename, $this->dataLong . $this->dataLong); - // Manually decrypt chunk - foreach ($e as $chunk) { + // Test that data was successfully written + $this->assertTrue(is_int($cryptedFile)); - $chunkDecrypt = Encryption\Crypt::symmetricDecryptFileContent($chunk, $plainKeyfile); + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; - // Assemble decrypted chunks - $decrypt .= $chunkDecrypt; + \OCP\Config::deleteSystemValue('cipher'); + + // Get file contents without using any wrapper to get it's actual contents on disk + $retreivedCryptedFile = $this->view->file_get_contents($this->userId . '/files/' . $filename); + + // Re-enable proxy - our work is done + \OC_FileProxy::$enabled = $proxyStatus; - } - $this->assertEquals($this->dataLong . $this->dataLong, $decrypt); + // Check that the file was encrypted before being written to disk + $this->assertNotEquals($this->dataLong . $this->dataLong, $retreivedCryptedFile); + + $decrypted = file_get_contents('crypt:///' . $this->userId . '/files/'. $filename); + + $this->assertEquals($this->dataLong . $this->dataLong, $decrypted); // Teardown @@ -294,14 +344,22 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { /** * @medium - * Test that data that is read by the crypto stream wrapper + * Test that data that is written by the crypto stream wrapper with AES 128 + * @note Encrypted data is manually prepared and decrypted here to avoid dependency on success of stream_read + * @note If this test fails with truncate content, check that enough array slices are being rejoined to form $e, as the crypt.php file may have gotten longer and broken the manual + * reassembly of its data */ - function testSymmetricStreamDecryptShortFileContent() { + function testStreamDecryptLongFileContentWithoutHeader() { - $filename = 'tmp-' . uniqid(); + // Generate a a random filename + $filename = 'tmp-' . uniqid() . '.test'; + + \OCP\Config::setSystemValue('cipher', 'AES-128-CFB'); // Save long data as encrypted file using stream wrapper - $cryptedFile = file_put_contents('crypt:///'. $this->userId . '/files/' . $filename, $this->dataShort); + $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/' . $filename, $this->dataLong . $this->dataLong); + + \OCP\Config::deleteSystemValue('cipher'); // Test that data was successfully written $this->assertTrue(is_int($cryptedFile)); @@ -310,39 +368,30 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - $this->assertTrue(Encryption\Crypt::isEncryptedMeta($filename)); - - \OC_FileProxy::$enabled = $proxyStatus; + // Get file contents without using any wrapper to get it's actual contents on disk + $retreivedCryptedFile = $this->view->file_get_contents($this->userId . '/files/' . $filename); - // Get file decrypted contents - $decrypt = file_get_contents('crypt:///' . $this->userId . '/files/' . $filename); + // Check that the file was encrypted before being written to disk + $this->assertNotEquals($this->dataLong . $this->dataLong, $retreivedCryptedFile); - $this->assertEquals($this->dataShort, $decrypt); + // remove the header to check if we can also decrypt old files without a header, + // this files should fall back to AES-128 + $cryptedWithoutHeader = substr($retreivedCryptedFile, Encryption\Crypt::BLOCKSIZE); + $this->view->file_put_contents($this->userId . '/files/' . $filename, $cryptedWithoutHeader); - // tear down - $this->view->unlink($this->userId . '/files/' . $filename); - } - - /** - * @medium - */ - function testSymmetricStreamDecryptLongFileContent() { + // Re-enable proxy - our work is done + \OC_FileProxy::$enabled = $proxyStatus; - $filename = 'tmp-' . uniqid(); + $decrypted = file_get_contents('crypt:///' . $this->userId . '/files/'. $filename); - // Save long data as encrypted file using stream wrapper - $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/' . $filename, $this->dataLong); + $this->assertEquals($this->dataLong . $this->dataLong, $decrypted); - // Test that data was successfully written - $this->assertTrue(is_int($cryptedFile)); + // Teardown - // Get file decrypted contents - $decrypt = file_get_contents('crypt:///' . $this->userId . '/files/' . $filename); + $this->view->unlink($this->userId . '/files/' . $filename); - $this->assertEquals($this->dataLong, $decrypt); + Encryption\Keymanager::deleteFileKey($this->view, $filename); - // tear down - $this->view->unlink($this->userId . '/files/' . $filename); } /** @@ -354,7 +403,7 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { $this->assertFalse(Encryption\Crypt::isCatfileContent($this->legacyEncryptedData)); - $keyfileContent = Encryption\Crypt::symmetricEncryptFileContent($this->dataUrl, 'hat'); + $keyfileContent = Encryption\Crypt::symmetricEncryptFileContent($this->dataUrl, 'hat', 'AES-128-CFB'); $this->assertTrue(Encryption\Crypt::isCatfileContent($keyfileContent)); diff --git a/apps/files_encryption/tests/keymanager.php b/apps/files_encryption/tests/keymanager.php index e779f8341e6..f90832280a2 100644 --- a/apps/files_encryption/tests/keymanager.php +++ b/apps/files_encryption/tests/keymanager.php @@ -107,7 +107,7 @@ class Test_Encryption_Keymanager extends \PHPUnit_Framework_TestCase { $key = Encryption\Keymanager::getPrivateKey($this->view, $this->userId); - $privateKey = Encryption\Crypt::symmetricDecryptFileContent($key, $this->pass); + $privateKey = Encryption\Crypt::decryptPrivateKey($key, $this->pass); $res = openssl_pkey_get_private($privateKey); @@ -177,6 +177,38 @@ class Test_Encryption_Keymanager extends \PHPUnit_Framework_TestCase { /** * @medium */ + function testSetPrivateKey() { + + $key = "dummy key"; + + Encryption\Keymanager::setPrivateKey($key, 'dummyUser'); + + $this->assertTrue($this->view->file_exists('/dummyUser/files_encryption/dummyUser.private.key')); + + //clean up + $this->view->deleteAll('/dummyUser'); + } + + /** + * @medium + */ + function testSetPrivateSystemKey() { + + $key = "dummy key"; + $keyName = "myDummyKey.private.key"; + + Encryption\Keymanager::setPrivateSystemKey($key, $keyName); + + $this->assertTrue($this->view->file_exists('/owncloud_private_key/' . $keyName)); + + // clean up + $this->view->unlink('/owncloud_private_key/' . $keyName); + } + + + /** + * @medium + */ function testGetUserKeys() { $keys = Encryption\Keymanager::getUserKeys($this->view, $this->userId); @@ -189,7 +221,7 @@ class Test_Encryption_Keymanager extends \PHPUnit_Framework_TestCase { $this->assertArrayHasKey('key', $sslInfoPublic); - $privateKey = Encryption\Crypt::symmetricDecryptFileContent($keys['privateKey'], $this->pass); + $privateKey = Encryption\Crypt::decryptPrivateKey($keys['privateKey'], $this->pass); $resPrivate = openssl_pkey_get_private($privateKey); diff --git a/apps/files_encryption/tests/share.php b/apps/files_encryption/tests/share.php index 7bbea6488bc..1f1304bb527 100755 --- a/apps/files_encryption/tests/share.php +++ b/apps/files_encryption/tests/share.php @@ -540,9 +540,9 @@ class Test_Encryption_Share extends \PHPUnit_Framework_TestCase { . $this->filename . '.' . $publicShareKeyId . '.shareKey')); // some hacking to simulate public link - $GLOBALS['app'] = 'files_sharing'; - $GLOBALS['fileOwner'] = \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1; - \OC_User::setUserId(false); + //$GLOBALS['app'] = 'files_sharing'; + //$GLOBALS['fileOwner'] = \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1; + \Test_Encryption_Util::logoutHelper(); // get file contents $retrievedCryptedFile = file_get_contents('crypt:///' . \Test_Encryption_Share::TEST_ENCRYPTION_SHARE_USER1 . '/files/' . $this->filename); diff --git a/apps/files_encryption/tests/util.php b/apps/files_encryption/tests/util.php index ae93e87d013..f337eb46355 100755 --- a/apps/files_encryption/tests/util.php +++ b/apps/files_encryption/tests/util.php @@ -528,7 +528,7 @@ class Test_Encryption_Util extends \PHPUnit_Framework_TestCase { public static function logoutHelper() { \OC_Util::tearDownFS(); - \OC_User::setUserId(''); + \OC_User::setUserId(false); \OC\Files\Filesystem::tearDown(); } |