Development snapshot, lots of fixes

Web UI based encryption working
Crypt and Util unit tests passing
This commit is contained in:
Sam Tuke 2012-11-16 18:31:37 +00:00
parent 6df35eeddc
commit 637891b771
8 changed files with 176 additions and 76 deletions

View File

@ -36,32 +36,40 @@ class Hooks {
*/ */
public static function login( $params ) { public static function login( $params ) {
if ( Crypt::mode( $params['uid'] ) == 'server' ) { // if ( Crypt::mode( $params['uid'] ) == 'server' ) {
# TODO: use lots of dependency injection here # TODO: use lots of dependency injection here
$view = new \OC_FilesystemView( '/' ); $view = new \OC_FilesystemView( '/' );
$util = new Util( $view, $params['uid'] ); $util = new Util( $view, $params['uid'] );
if ( !$util->ready()) { if ( ! $util->ready() ) {
\OC_Log::write( 'Encryption library', 'User account "' . $params['uid'] . '" is not ready for encryption; configuration started' , \OC_Log::DEBUG );
return $util->setupServerSide( $params['password'] ); return $util->setupServerSide( $params['password'] );
} }
\OC_FileProxy::$enabled = false; \OC_FileProxy::$enabled = false;
$encryptedKey = Keymanager::getPrivateKey( $params['uid'], $view ); $encryptedKey = Keymanager::getPrivateKey( $params['uid'], $view );
\OC_FileProxy::$enabled = true; \OC_FileProxy::$enabled = true;
# TODO: dont manually encrypt the private keyfile - use the config options of openssl_pkey_export instead for better mobile compatibility # TODO: dont manually encrypt the private keyfile - use the config options of openssl_pkey_export instead for better mobile compatibility
//trigger_error( "\$encryptedKey = ".var_export($encryptedKey)." \n\n\$params['password'] = ".var_export($params['password'] ) );
// trigger_error( "\$params['password'] = {$params['password']}" );
$_SESSION['enckey'] = Crypt::symmetricDecryptFileContent( $encryptedKey, $params['password'] ); $_SESSION['enckey'] = Crypt::symmetricDecryptFileContent( $encryptedKey, $params['password'] );
} // trigger_error( "\$_SESSION['enckey'] = {$_SESSION['enckey']}" );
// }
return true; return true;

View File

@ -45,23 +45,26 @@ class Crypt {
*/ */
public static function mode( $user = null ) { public static function mode( $user = null ) {
$mode = \OC_Appconfig::getValue( 'files_encryption', 'mode', 'none' ); // $mode = \OC_Appconfig::getValue( 'files_encryption', 'mode', 'none' );
//
// if ( $mode == 'user') {
// if ( !$user ) {
// $user = \OCP\User::getUser();
// }
// $mode = 'none';
// if ( $user ) {
// $query = \OC_DB::prepare( "SELECT mode FROM *PREFIX*encryption WHERE uid = ?" );
// $result = $query->execute(array($user));
// if ($row = $result->fetchRow()){
// $mode = $row['mode'];
// }
// }
// }
//
// return $mode;
if ( $mode == 'user') { return 'server';
if ( !$user ) {
$user = \OCP\User::getUser();
}
$mode = 'none';
if ( $user ) {
$query = \OC_DB::prepare( "SELECT mode FROM *PREFIX*encryption WHERE uid = ?" );
$result = $query->execute(array($user));
if ($row = $result->fetchRow()){
$mode = $row['mode'];
}
}
}
return $mode;
} }
/** /**
@ -101,7 +104,7 @@ class Crypt {
/** /**
* @brief Remove arbitrary padding to encrypted data * @brief Remove arbitrary padding to encrypted data
* @param string $padded padded data to remove padding from * @param string $padded padded data to remove padding from
* @return padded data on success, false on error * @return unpadded data on success, false on error
*/ */
public static function removePadding( $padded ) { public static function removePadding( $padded ) {
@ -220,7 +223,7 @@ class Crypt {
} else { } else {
\OC_Log::write( 'Encryption library', 'Decryption (symmetric) of content failed' , \OC_Log::ERROR ); throw new \Exception( 'Encryption library: Decryption (symmetric) of content failed' );
return false; return false;
@ -317,7 +320,7 @@ class Crypt {
if ( !$keyfileContent ) { if ( !$keyfileContent ) {
return false; throw new \Exception( 'Encryption library: no data provided for decryption' );
} }
@ -330,16 +333,12 @@ class Crypt {
// Remove IV and IV identifier text to expose encrypted content // Remove IV and IV identifier text to expose encrypted content
$encryptedContent = substr( $noPadding, 0, -22 ); $encryptedContent = substr( $noPadding, 0, -22 );
//trigger_error( "\n\n\$noPadding = ".var_export($noPadding)."\n\n\$iv = ".var_export($iv )."\n\n\$encryptedContent = ".var_export($encryptedContent) );
if ( $plainContent = self::decrypt( $encryptedContent, $iv, $passphrase ) ) { if ( $plainContent = self::decrypt( $encryptedContent, $iv, $passphrase ) ) {
return $plainContent; return $plainContent;
} else {
\OC_Log::write( 'Encryption library', 'Decryption (symmetric) of keyfile content failed' , \OC_Log::ERROR );
return false;
} }
} }

View File

@ -230,7 +230,7 @@ class Keymanager {
* @return bool true/false * @return bool true/false
*/ */
public static function setFileKey( $path, $key, $view = Null, $dbClassName = '\OC_DB') { public static function setFileKey( $path, $key, $view = Null, $dbClassName = '\OC_DB') {
var_dump($path);
$targetPath = ltrim( $path, '/' ); $targetPath = ltrim( $path, '/' );
$user = \OCP\User::getUser(); $user = \OCP\User::getUser();
@ -274,7 +274,10 @@ class Keymanager {
$view->chroot( '/' . $user . '/files_encryption/keyfiles' ); $view->chroot( '/' . $user . '/files_encryption/keyfiles' );
// If the file resides within a subdirectory, create it // If the file resides within a subdirectory, create it
if ( ! $view->file_exists( $path_parts['dirname'] ) ) { if (
isset( $path_parts['dirname'] )
&& ! $view->file_exists( $path_parts['dirname'] )
) {
$view->mkdir( $path_parts['dirname'] ); $view->mkdir( $path_parts['dirname'] );

View File

@ -46,23 +46,34 @@ class Proxy extends \OC_FileProxy {
if ( is_null( self::$enableEncryption ) ) { if ( is_null( self::$enableEncryption ) ) {
self::$enableEncryption = ( \OCP\Config::getAppValue( 'files_encryption', 'enable_encryption', 'true' ) == 'true' && Crypt::mode() == 'server' ); if (
\OCP\Config::getAppValue( 'files_encryption', 'enable_encryption', 'true' ) == 'true'
&& Crypt::mode() == 'server'
) {
self::$enableEncryption = true;
} else {
self::$enableEncryption = false;
}
} }
if( !self::$enableEncryption ) { if ( !self::$enableEncryption ) {
return false; return false;
} }
if( is_null(self::$blackList ) ) { if ( is_null(self::$blackList ) ) {
self::$blackList = explode(',', \OCP\Config::getAppValue( 'files_encryption','type_blacklist','jpg,png,jpeg,avi,mpg,mpeg,mkv,mp3,oga,ogv,ogg' ) ); self::$blackList = explode(',', \OCP\Config::getAppValue( 'files_encryption','type_blacklist','jpg,png,jpeg,avi,mpg,mpeg,mkv,mp3,oga,ogv,ogg' ) );
} }
if( Crypt::isEncryptedContent( $path ) ) { if ( Crypt::isEncryptedContent( $path ) ) {
return true; return true;
@ -132,6 +143,7 @@ class Proxy extends \OC_FileProxy {
} }
} }
} }
public function postFile_get_contents( $path, $data ) { public function postFile_get_contents( $path, $data ) {

View File

@ -31,9 +31,18 @@ namespace OCA\Encryption;
/** /**
* @brief Provides 'crypt://' stream wrapper protocol. * @brief Provides 'crypt://' stream wrapper protocol.
* @note We use a stream wrapper because it is the most secure way to handle decrypted content transfers. There is no safe way to decrypt the entire file somewhere on the server, so we have to encrypt and decrypt blocks on the fly. * @note We use a stream wrapper because it is the most secure way to handle
* @note Paths used with this protocol MUST BE RELATIVE, due to limitations of OC_FilesystemView. crypt:///home/user/owncloud/data <- will put keyfiles in [owncloud]/data/user/files_encryption/keyfiles/home/user/owncloud/data and will not be accessible by other functions. * decrypted content transfers. There is no safe way to decrypt the entire file
* @note Data read and written must always be 8192 bytes long, as this is the buffer size used internally by PHP. The encryption process makes the input data longer, and input is chunked into smaller pieces in order to result in a 8192 encrypted block size. * somewhere on the server, so we have to encrypt and decrypt blocks on the fly.
* @note Paths used with this protocol MUST BE RELATIVE. Use URLs like:
* crypt://filename, or crypt://subdirectory/filename, NOT
* crypt:///home/user/owncloud/data. Otherwise keyfiles will be put keyfiles in
* [owncloud]/data/user/files_encryption/keyfiles/home/user/owncloud/data and
* will not be accessible to other functions.
* @note Data read and written must always be 8192 bytes long, as this is the
* buffer size used internally by PHP. The encryption process makes the input
* data longer, and input is chunked into smaller pieces in order to result in
* a 8192 encrypted block size.
*/ */
class Stream { class Stream {
@ -41,6 +50,8 @@ class Stream {
# TODO: make all below properties private again once unit testing is configured correctly # TODO: make all below properties private again once unit testing is configured correctly
public $rawPath; // The raw path received by stream_open public $rawPath; // The raw path received by stream_open
public $path_f; // The raw path formatted to include username and data directory
private $userId;
private $handle; // Resource returned by fopen private $handle; // Resource returned by fopen
private $path; private $path;
private $readBuffer; // For streams that dont support seeking private $readBuffer; // For streams that dont support seeking
@ -53,20 +64,24 @@ class Stream {
public function stream_open( $path, $mode, $options, &$opened_path ) { public function stream_open( $path, $mode, $options, &$opened_path ) {
file_put_contents('/home/samtuke/newtmp.txt', 'stream_open('.$path.')' ); //file_put_contents('/home/samtuke/newtmp.txt', 'stream_open('.$path.')' );
// Get access to filesystem via filesystemview object // Get access to filesystem via filesystemview object
if ( !self::$view ) { if ( !self::$view ) {
self::$view = new \OC_FilesystemView( '' ); self::$view = new \OC_FilesystemView( $this->userId . '/' );
} }
$this->userId = \OCP\User::getUser();
// Get the bare file path // Get the bare file path
$path = str_replace( 'crypt://', '', $path ); $path = str_replace( 'crypt://', '', $path );
$this->rawPath = $path; $this->rawPath = $path;
$this->path_f = $this->userId . '/files/' . $path;
if ( if (
dirname( $path ) == 'streams' dirname( $path ) == 'streams'
and isset( self::$sourceStreams[basename( $path )] ) and isset( self::$sourceStreams[basename( $path )] )
@ -95,7 +110,7 @@ class Stream {
$this->size = self::$view->filesize( $path, $mode ); $this->size = self::$view->filesize( $this->path_f, $mode );
//$this->size = filesize( $path ); //$this->size = filesize( $path );
@ -106,7 +121,7 @@ class Stream {
//$this->handle = fopen( $path, $mode ); //$this->handle = fopen( $path, $mode );
$this->handle = self::$view->fopen( $path, $mode ); $this->handle = self::$view->fopen( $this->path_f, $mode );
//file_put_contents('/home/samtuke/newtmp.txt', 'fucking hopeless = '.$path ); //file_put_contents('/home/samtuke/newtmp.txt', 'fucking hopeless = '.$path );
@ -165,7 +180,7 @@ class Stream {
//echo "\n\nPRE DECRYPTION = $data\n\n"; //echo "\n\nPRE DECRYPTION = $data\n\n";
// //
// if ( strlen( $data ) ) { if ( strlen( $data ) ) {
$this->getKey(); $this->getKey();
@ -186,14 +201,12 @@ class Stream {
// echo "\n\n\n\n-----------------------------\n\n"; // echo "\n\n\n\n-----------------------------\n\n";
//trigger_error("CAT $result"); //trigger_error("CAT $result");
} else {
$result = '';
// } else { }
//
// $result = '';
//
// }
// $length = $this->size - $pos; // $length = $this->size - $pos;
// //
@ -234,14 +247,11 @@ class Stream {
* @return bool true on key found and set, false on key not found and new key generated and set * @return bool true on key found and set, false on key not found and new key generated and set
*/ */
public function getKey( $generate = true ) { public function getKey( $generate = true ) {
# TODO: Move this user call out of here - it belongs elsewhere
$user = \OCP\User::getUser();
//echo "\n\$this->rawPath = {$this->rawPath}"; //echo "\n\$this->rawPath = {$this->rawPath}";
// If a keyfile already exists for a file named identically to file to be written // If a keyfile already exists for a file named identically to file to be written
if ( self::$view->file_exists( $user . '/'. 'files_encryption' . '/' . 'keyfiles' . '/' . $this->rawPath . '.key' ) ) { if ( self::$view->file_exists( $this->userId . '/'. 'files_encryption' . '/' . 'keyfiles' . '/' . $this->rawPath . '.key' ) ) {
# TODO: add error handling for when file exists but no keyfile # TODO: add error handling for when file exists but no keyfile
@ -276,6 +286,8 @@ class Stream {
*/ */
public function stream_write( $data ) { public function stream_write( $data ) {
// file_put_contents('/home/samtuke/newtmp.txt', 'stream_write('.$data.')' ); // file_put_contents('/home/samtuke/newtmp.txt', 'stream_write('.$data.')' );
// 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 // 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

View File

@ -65,6 +65,11 @@ class Util {
private $view; // OC_FilesystemView object for filesystem operations private $view; // OC_FilesystemView object for filesystem operations
private $pwd; // User Password private $pwd; // User Password
private $client; // Client side encryption mode flag private $client; // Client side encryption mode flag
private $publicKeyDir; // Directory containing all public user keys
private $encryptionDir; // Directory containing user's files_encryption
private $keyfilesPath; // Directory containing user's keyfiles
private $publicKeyPath; // Path to user's public key
private $privateKeyPath; // Path to user's private key
public function __construct( \OC_FilesystemView $view, $userId, $client = false ) { public function __construct( \OC_FilesystemView $view, $userId, $client = false ) {
@ -102,11 +107,6 @@ class Util {
* @param $passphrase passphrase to encrypt server-stored private key with * @param $passphrase passphrase to encrypt server-stored private key with
*/ */
public function setupServerSide( $passphrase = null ) { public function setupServerSide( $passphrase = null ) {
// Log changes to user's filesystem
$this->appInfo = \OC_APP::getAppInfo( 'files_encryption' );
\OC_Log::write( $this->appInfo['name'], 'File encryption for user "' . $this->userId . '" will be set up' , \OC_Log::INFO );
// Create shared public key directory // Create shared public key directory
if( !$this->view->file_exists( $this->publicKeyDir ) ) { if( !$this->view->file_exists( $this->publicKeyDir ) ) {
@ -152,6 +152,8 @@ class Util {
\OC_FileProxy::$enabled = true; \OC_FileProxy::$enabled = true;
} }
return true;
} }
@ -357,5 +359,43 @@ class Util {
# TODO: write me # TODO: write me
} }
public function getPath( $pathName ) {
switch ( $pathName ) {
case 'publicKeyDir':
return $this->publicKeyDir;
break;
case 'encryptionDir':
return $this->encryptionDir;
break;
case 'keyfilesPath':
return $this->keyfilesPath;
break;
case 'publicKeyPath':
return $this->publicKeyPath;
break;
case 'privateKeyPath':
return $this->privateKeyPath;
break;
}
}
} }

View File

@ -34,7 +34,9 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$this->view = new \OC_FilesystemView( '/' ); $this->view = new \OC_FilesystemView( '/' );
\OC_User::setUserId( 'admin' ); $this->userId = 'admin';
\OC_User::setUserId( $this->userId );
} }
@ -109,6 +111,29 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
} }
function testAddPadding() {
$padded = Encryption\Crypt::addPadding( $this->dataLong );
$padding = substr( $padded, -2 );
$this->assertEquals( 'xx' , $padding );
return $padded;
}
/**
* @depends testAddPadding
*/
function testRemovePadding( $padded ) {
$noPadding = Encryption\Crypt::RemovePadding( $padded );
$this->assertEquals( $this->dataLong, $noPadding );
}
function testEncrypt() { function testEncrypt() {
$random = openssl_random_pseudo_bytes( 13 ); $random = openssl_random_pseudo_bytes( 13 );
@ -188,7 +213,7 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
// Get file contents without using any wrapper to get it's actual contents on disk // Get file contents without using any wrapper to get it's actual contents on disk
$retreivedCryptedFile = $this->view->file_get_contents( $filename ); $retreivedCryptedFile = $this->view->file_get_contents( $this->userId . '/files/' . $filename );
//echo "\n\n\$retreivedCryptedFile = $retreivedCryptedFile"; //echo "\n\n\$retreivedCryptedFile = $retreivedCryptedFile";
@ -222,7 +247,7 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$this->assertTrue( is_int( $cryptedFile ) ); $this->assertTrue( is_int( $cryptedFile ) );
// Get file contents without using any wrapper to get it's actual contents on disk // Get file contents without using any wrapper to get it's actual contents on disk
$retreivedCryptedFile = $this->view->file_get_contents( $filename ); $retreivedCryptedFile = $this->view->file_get_contents( $this->userId . '/files/' . $filename );
// echo "\n\n\$retreivedCryptedFile = $retreivedCryptedFile\n\n"; // echo "\n\n\$retreivedCryptedFile = $retreivedCryptedFile\n\n";
@ -261,17 +286,16 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$this->assertEquals( $this->dataLong.$this->dataLong, $decrypt ); $this->assertEquals( $this->dataLong.$this->dataLong, $decrypt );
// Teadown // Teardown
$this->view->unlink( $filename ); $this->view->unlink( $filename );
Keymanager::deleteFileKey( $filename ); Encryption\Keymanager::deleteFileKey( $filename );
} }
/** /**
* @brief Test that data that is read by the crypto stream wrapper * @brief Test that data that is read by the crypto stream wrapper
* @depends testSymmetricStreamEncryptLongFileContent
*/ */
function testSymmetricStreamDecryptShortFileContent() { function testSymmetricStreamDecryptShortFileContent() {
@ -285,7 +309,7 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
// Get file contents without using any wrapper to get it's actual contents on disk // Get file contents without using any wrapper to get it's actual contents on disk
$retreivedCryptedFile = $this->view->file_get_contents( $filename ); $retreivedCryptedFile = $this->view->file_get_contents( $this->userId . '/files/' . $filename );
$decrypt = file_get_contents( 'crypt://' . $filename ); $decrypt = file_get_contents( 'crypt://' . $filename );
@ -305,7 +329,7 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
// Get file contents without using any wrapper to get it's actual contents on disk // Get file contents without using any wrapper to get it's actual contents on disk
$retreivedCryptedFile = $this->view->file_get_contents( '/admin/files/' . $filename ); $retreivedCryptedFile = $this->view->file_get_contents( $this->userId . '/files/' . $filename );
$decrypt = file_get_contents( 'crypt://' . $filename ); $decrypt = file_get_contents( 'crypt://' . $filename );

View File

@ -5,12 +5,12 @@
* later. * later.
* See the COPYING-README file. * See the COPYING-README file.
*/ */
namespace OCA\Encryption;
require_once "PHPUnit/Framework/TestCase.php"; require_once "PHPUnit/Framework/TestCase.php";
require_once realpath( dirname(__FILE__).'/../../../lib/base.php' ); require_once realpath( dirname(__FILE__).'/../../../lib/base.php' );
use OCA\Encryption;
class Test_Keymanager extends \PHPUnit_Framework_TestCase { class Test_Keymanager extends \PHPUnit_Framework_TestCase {
function setUp() { function setUp() {
@ -34,7 +34,7 @@ class Test_Keymanager extends \PHPUnit_Framework_TestCase {
function testGetEncryptedPrivateKey() { function testGetEncryptedPrivateKey() {
$key = Keymanager::getPrivateKey( $this->user, $this->view ); $key = Encryption\Keymanager::getPrivateKey( $this->user, $this->view );
$this->assertEquals( 2302, strlen( $key ) ); $this->assertEquals( 2302, strlen( $key ) );
@ -52,16 +52,18 @@ class Test_Keymanager extends \PHPUnit_Framework_TestCase {
// //
// //$view = new \OC_FilesystemView( '/' . $this->user . '/files_encryption/keyfiles' ); // //$view = new \OC_FilesystemView( '/' . $this->user . '/files_encryption/keyfiles' );
// //
// Keymanager::setFileKey( $tmpPath, $key['key'], $view ); // Encryption\Keymanager::setFileKey( $tmpPath, $key['key'], $view );
} }
function testGetDecryptedPrivateKey() { function testGetDecryptedPrivateKey() {
$key = Keymanager::getPrivateKey( $this->user, $this->view ); $key = Encryption\Keymanager::getPrivateKey( $this->user, $this->view );
# TODO: replace call to Crypt with a mock object? # TODO: replace call to Crypt with a mock object?
$decrypted = Crypt::symmetricDecryptFileContent( $key, $this->passphrase ); $decrypted = Encryption\Crypt::symmetricDecryptFileContent( $key, $this->passphrase );
var_dump($decrypted);
$this->assertEquals( 1708, strlen( $decrypted ) ); $this->assertEquals( 1708, strlen( $decrypted ) );