Extensive work on crypto stream wrapper implementation

This commit is contained in:
Sam Tuke 2012-08-23 16:43:10 +01:00
parent 293a0f4d32
commit 32ee3de918
5 changed files with 261 additions and 100 deletions

View file

@ -177,6 +177,14 @@ class Crypt {
}
public static function concatIv ( $content, $iv ) {
$combined = $content . '00iv00' . $iv;
return $combined;
}
/**
* @brief Symmetrically encrypts a string and returns keyfile content
* @param $plainContent content to be encrypted in keyfile
@ -197,7 +205,7 @@ class Crypt {
if ( $encryptedContent = self::encrypt( $plainContent, $iv, $passphrase ) ) {
// Combine content to encrypt with IV identifier and actual IV
$combinedKeyfile = $encryptedContent . '00iv00' . $iv;
$combinedKeyfile = self::concatIv( $encryptedContent, $iv );
return $combinedKeyfile;
@ -398,13 +406,17 @@ class Crypt {
$crypted = '';
while( strlen( $plainContent ) ) {
$remaining = $plainContent;
while( strlen( $remaining ) ) {
// Encrypt a chunk of unencrypted data and add it to the rest
$crypted .= self::symmetricEncryptFileContent( substr( $plainContent, 0, 8192 ), $key );
$block = self::symmetricEncryptFileContent( substr( $remaining, 0, 8192 ), $key );
$crypted .= $block;
// Remove the data already encrypted from remaining unencrypted data
$plainContent = substr( $plainContent, 8192 );
$remaining = substr( $remaining, 8192 );
}
@ -418,17 +430,24 @@ class Crypt {
*/
public static function symmetricBlockDecryptFileContent( $crypted, $key ) {
//echo "\n\n\nfags \$crypted = $crypted\n\n\n";
$decrypted = '';
while( strlen( $crypted ) ) {
$remaining = $crypted;
$decrypted .= self::symmetricDecryptFileContent( substr( $crypted, 0, 8192 ), $key );
while( strlen( $remaining ) ) {
// Encrypt a chunk of unencrypted data and add it to the rest
// 10946 is the length of a 8192 string once it has been encrypted
$decrypted .= self::symmetricDecryptFileContent( substr( $remaining, 0, 10946 ), $key );
$crypted = substr( $crypted, 8192 );
// Remove the data already encrypted from remaining unencrypted data
$remaining = substr( $remaining, 10946 );
}
return rtrim( $decrypted, "\0" );
return $decrypted;
}

View file

@ -197,45 +197,57 @@ class Keymanager {
*/
public static function setFileKey( $path, $key, $view = Null, $dbClassName = '\OC_DB') {
$targetpath = ltrim( $path, '/' );
$targetPath = ltrim( $path, '/' );
$user = \OCP\User::getUser();
// update $keytarget and $user if key belongs to a file shared by someone else
$query = $dbClassName::prepare( "SELECT uid_owner, source, target FROM `*PREFIX*sharing` WHERE target = ? AND uid_shared_with = ?" );
$result = $query->execute( array ( '/'.$user.'/files/'.$targetpath, $user ) );
$result = $query->execute( array ( '/'.$user.'/files/'.$targetPath, $user ) );
if ( $row = $result->fetchRow( ) ) {
$targetpath = $row['source'];
$targetPath = $row['source'];
$targetpath_parts=explode( '/',$targetpath );
$targetPath_parts = explode( '/', $targetPath );
$user = $targetpath_parts[1];
$user = $targetPath_parts[1];
$rootview = new \OC_FilesystemView( '/');
if (!$rootview->is_writable($targetpath)) {
\OC_Log::write( 'Encryption library', "File Key not updated because you don't have write access for the corresponding file" , \OC_Log::ERROR );
return false;
}
$rootview = new \OC_FilesystemView( '/' );
$targetpath = str_replace( '/'.$user.'/files/', '', $targetpath );
if ( ! $rootview->is_writable( $targetPath ) ) {
\OC_Log::write( 'Encryption library', "File Key not updated because you don't have write access for the corresponding file", \OC_Log::ERROR );
return false;
}
$targetPath = str_replace( '/'.$user.'/files/', '', $targetPath );
//TODO: check for write permission on shared file once the new sharing API is in place
}
$path_parts = pathinfo( $targetpath );
if (!$view) {
$path_parts = pathinfo( $targetPath );
if ( !$view ) {
$view = new \OC_FilesystemView( '/' );
}
$view->chroot( '/' . $user . '/files_encryption/keyfiles' );
if ( !$view->file_exists( $path_parts['dirname'] ) ) $view->mkdir( $path_parts['dirname'] );
// If the file resides within a subdirectory, create it
if ( ! $view->file_exists( $path_parts['dirname'] ) ) {
return $view->file_put_contents( '/' . $targetpath . '.key', $key );
$view->mkdir( $path_parts['dirname'] );
}
// Save the keyfile in parallel directory
return $view->file_put_contents( '/' . $targetPath . '.key', $key );
}

View file

@ -34,11 +34,13 @@ class Stream {
public static $sourceStreams = array();
private $source;
private $path;
private $rawPath; // The raw path received by stream_open
private $readBuffer; // For streams that dont support seeking
private $meta = array(); // Header / meta for source stream
private $count;
private $writeCache;
private $size;
private $keyfile;
private static $view;
public function stream_open( $path, $mode, $options, &$opened_path ) {
@ -52,7 +54,9 @@ class Stream {
// Get the bare file path
$path = str_replace( 'crypt://', '', $path );
$this->rawPath = $path;
if (
dirname( $path ) == 'streams'
and isset( self::$sourceStreams[basename( $path )] )
@ -115,33 +119,77 @@ class Stream {
return ftell($this->source);
}
public function stream_read($count) {
//$count will always be 8192 https://bugs.php.net/bug.php?id=21641
//This makes this function a lot simpler but will breake everything the moment it's fixed
$this->writeCache='';
if ($count!=8192) {
OCP\Util::writeLog('files_encryption','php bug 21641 no longer holds, decryption will not work',OCP\Util::FATAL);
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'
\OCP\Util::writeLog( 'files_encryption', 'PHP "bug" 21641 no longer holds, decryption system requires refactoring', OCP\Util::FATAL );
die();
}
$pos=ftell($this->source);
$data=fread($this->source,8192);
if (strlen($data)) {
$result=Crypt::decrypt($data);
}else{
$result='';
$pos = ftell( $this->source );
$data = fread( $this->source, 8192 );
if ( strlen( $data ) ) {
$result = Crypt::symmetricDecryptFileContent( $data, $this->keyfile );
} else {
$result = '';
}
$length=$this->size-$pos;
if ($length<8192) {
$result=substr($result,0,$length);
$length = $this->size - $pos;
if ( $length < 8192 ) {
$result = substr( $result, 0, $length );
}
return $result;
}
/**
* @brief Get the keyfile for the current file, generate one if necessary
*/
public function getKey() {
# TODO: Move this user call out of here - it belongs elsewhere
$user = \OCP\User::getUser();
if ( self::$view->file_exists( $this->rawPath . $user ) ) {
// If the data is to be written to an existing file, fetch its keyfile
$this->keyfile = Keymanager::getFileKey( $this->rawPath . $user );
} else {
// If the data is to be written to a new file, generate a new keyfile
$this->keyfile = Crypt::generateKey();
}
}
/**
* @brief
*/
public function stream_write( $data ) {
# TODO: Find a way to get path of file in order to know where to save its parallel keyfile
\OC_FileProxy::$enabled = false;
$length = strlen( $data );
$written = 0;
@ -151,15 +199,13 @@ class Stream {
# TODO: Move this user call out of here - it belongs elsewhere
$user = \OCP\User::getUser();
if ( self::$view->file_exists( $this->path . $user ) ) {
// Set keyfile property for file in question
$this->getKey();
$key = Keymanager::getFileKey( $this->path . $user );
if ( ! self::$view->file_exists( $this->rawPath . $user ) ) {
} else {
$key = Crypt::generateKey();
Keymanager::setFileKey( $path, $key, new \OC_FilesystemView );
// Save keyfile in parallel directory structure
Keymanager::setFileKey( $this->rawPath, $this->keyfile, new \OC_FilesystemView( '/' ) );
}
@ -180,7 +226,7 @@ class Stream {
fseek( $this->source, - ( $currentPos % 8192 ), SEEK_CUR );
$block = Crypt::symmetricDecryptFileContent( $encryptedBlock, $key );
$block = Crypt::symmetricDecryptFileContent( $encryptedBlock, $this->keyfile );
$data = substr( $block, 0, $currentPos % 8192 ) . $data;
@ -190,9 +236,9 @@ class Stream {
$currentPos = ftell( $this->source );
while( $remainingLength = strlen( $data )>0 ) {
while( $remainingLength = strlen( $data ) > 0 ) {
if ( $remainingLength<8192 ) {
if ( $remainingLength < 8192 ) {
$this->writeCache = $data;
@ -200,9 +246,11 @@ class Stream {
} else {
$encrypted = Crypt::symmetricBlockEncryptFileContent( $data, $key );
$encrypted = Crypt::symmetricBlockEncryptFileContent( $data, $this->keyfile );
fwrite( $this->source . $user, $encrypted );
//$encrypted = $data;
fwrite( $this->source, $encrypted );
$data = substr( $data,8192 );
@ -255,10 +303,17 @@ class Stream {
}
public function stream_close() {
$this->flush();
if ($this->meta['mode']!='r' and $this->meta['mode']!='rb') {
OC_FileCache::put($this->path,array('encrypted'=>true,'size'=>$this->size),'');
\OC_FileCache::put($this->path,array('encrypted'=>true,'size'=>$this->size),'');
}
return fclose($this->source);
}
}

View file

@ -17,9 +17,13 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
function setUp() {
// set content for encrypting / decrypting in tests
$this->data = realpath( dirname(__FILE__).'/../lib/crypt.php' );
$this->dataLong = file_get_contents( realpath( dirname(__FILE__).'/../lib/crypt.php' ) );
$this->dataShort = 'hats';
$this->dataUrl = realpath( dirname(__FILE__).'/../lib/crypt.php' );
$this->legacyData = realpath( dirname(__FILE__).'/legacy-text.txt' );
$this->legacyEncryptedData = realpath( dirname(__FILE__).'/legacy-encrypted-text.txt' );
$this->view = new \OC_FilesystemView( '/' );
//stream_wrapper_register( 'crypt', 'OCA_Encryption\Stream' );
@ -51,9 +55,9 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$iv = substr( base64_encode( $random ), 0, -4 ); // i.e. E5IG033j+mRNKrht
$crypted = Crypt::encrypt( $this->data, $iv, 'hat' );
$crypted = Crypt::encrypt( $this->dataUrl, $iv, 'hat' );
$this->assertNotEquals( $this->data, $crypted );
$this->assertNotEquals( $this->dataUrl, $crypted );
}
@ -63,11 +67,11 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$iv = substr( base64_encode( $random ), 0, -4 ); // i.e. E5IG033j+mRNKrht
$crypted = Crypt::encrypt( $this->data, $iv, 'hat' );
$crypted = Crypt::encrypt( $this->dataUrl, $iv, 'hat' );
$decrypt = Crypt::decrypt( $crypted, $iv, 'hat' );
$this->assertEquals( $this->data, $decrypt );
$this->assertEquals( $this->dataUrl, $decrypt );
}
@ -75,81 +79,133 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
# TODO: search in keyfile for actual content as IV will ensure this test always passes
$crypted = Crypt::symmetricEncryptFileContent( $this->data, 'hat' );
$crypted = Crypt::symmetricEncryptFileContent( $this->dataUrl, 'hat' );
$this->assertNotEquals( $this->data, $crypted );
$this->assertNotEquals( $this->dataUrl, $crypted );
$decrypt = Crypt::symmetricDecryptFileContent( $crypted, 'hat' );
$this->assertEquals( $this->data, $decrypt );
$this->assertEquals( $this->dataUrl, $decrypt );
}
function testSymmetricBlockEncryptFileContent() {
function testSymmetricBlockEncryptShortFileContent() {
$crypted = Crypt::symmetricBlockEncryptFileContent( $this->data, 'hat' );
$this->assertNotEquals( $this->data, $crypted );
$key = file_get_contents( '/home/samtuke/owncloud/git/oc3/data/admin/files_encryption/keyfiles/sscceEncrypt-1345649062.key' );
$crypted = Crypt::symmetricBlockEncryptFileContent( $this->dataShort, $key );
$this->assertNotEquals( $this->dataShort, $crypted );
$decrypt = Crypt::symmetricBlockDecryptFileContent( $crypted, 'hat' );
$decrypt = Crypt::symmetricBlockDecryptFileContent( $crypted, $key );
$this->assertEquals( $this->data, $decrypt );
$this->assertEquals( $this->dataShort, $decrypt );
}
function testSymmetricBlockEncryptLongFileContent() {
$key = file_get_contents( '/home/samtuke/owncloud/git/oc3/data/admin/files_encryption/keyfiles/sscceEncrypt-1345649062.key' );
$crypted = Crypt::symmetricBlockEncryptFileContent( substr( $this->dataLong, 0, 6500 ), $key );
$this->assertNotEquals( $this->dataLong, $crypted );
//echo "\n\nCAT ".substr( $this->dataLong, 0, 7000 );
$decrypt = Crypt::symmetricBlockDecryptFileContent( $crypted, $key );
$this->assertEquals( substr( $this->dataLong, 0, 6500
), $decrypt );
}
// function testSymmetricBlockStreamEncryptFileContent() {
//
// $crypted = Crypt::symmetricBlockEncryptFileContent( $this->data, 'hat' );
// \OC_User::setUserId( 'admin' );
//
// $cryptedFile = file_put_contents( 'crypt://' . '/blockEncrypt', $crypted );
// // Disable encryption proxy to prevent unwanted en/decryption
// \OC_FileProxy::$enabled = false;
//
// $cryptedFile = file_put_contents( 'crypt://' . '/blockEncrypt', $this->dataUrl );
//
// // Test that data was successfully written
// $this->assertTrue( $cryptedFile );
// $this->assertTrue( is_int( $cryptedFile ) );
//
// $retreivedCryptedFile = file_get_contents( '/blockEncrypt' );
// // Disable encryption proxy to prevent unwanted en/decryption
// \OC_FileProxy::$enabled = false;
//
// $this->assertNotEquals( $this->data, $retreivedCryptedFile );
//
//
// // Get file contents without using any wrapper to get it's actual contents on disk
// $retreivedCryptedFile = $this->view->file_get_contents( '/blockEncrypt' );
//
// echo "\n\n\$retreivedCryptedFile = !! $retreivedCryptedFile !!";
//
// $key = file_get_contents( '/home/samtuke/owncloud/git/oc3/data/files_encryption/keyfiles/tmp/testSetFileKey.key' );
//
// echo "\n\n\$key = !! $key !!";
//
// $manualDecrypt = Crypt::symmetricDecryptFileContent( $retreivedCryptedFile, $key );
//
// echo "\n\n\$manualDecrypt = !! $manualDecrypt !!";
//
// // Check that the file was encrypted before being written to disk
// $this->assertNotEquals( $this->dataUrl, $retreivedCryptedFile );
//
// $decrypt = Crypt::symmetricBlockDecryptFileContent( $retreivedCryptedFile, $key);
//
// $this->assertEquals( $this->dataUrl, $decrypt );
//
// }
function testSymmetricBlockStreamDecryptFileContent() {
\OC_User::setUserId( 'admin' );
$crypted = Crypt::symmetricBlockEncryptFileContent( $this->data, 'hat' );
$cryptedFile = file_put_contents( 'crypt://' . '/blockEncrypt', $crypted );
$retreivedCryptedFile = file_get_contents( 'crypt://' . '/blockEncrypt' );
$this->assertEquals( $this->data, $retreivedCryptedFile );
}
// function testSymmetricBlockStreamDecryptFileContent() {
//
// \OC_User::setUserId( 'admin' );
//
// // Disable encryption proxy to prevent unwanted en/decryption
// \OC_FileProxy::$enabled = false;
//
// $cryptedFile = file_put_contents( 'crypt://' . '/blockEncrypt', $this->dataUrl );
//
// // Disable encryption proxy to prevent unwanted en/decryption
// \OC_FileProxy::$enabled = false;
//
// echo "\n\n\$cryptedFile = " . $this->view->file_get_contents( '/blockEncrypt' );
//
// $retreivedCryptedFile = file_get_contents( 'crypt://' . '/blockEncrypt' );
//
// $this->assertEquals( $this->dataUrl, $retreivedCryptedFile );
//
// \OC_FileProxy::$enabled = false;
//
// }
function testSymmetricEncryptFileContentKeyfile() {
# TODO: search in keyfile for actual content as IV will ensure this test always passes
$crypted = Crypt::symmetricEncryptFileContentKeyfile( $this->data );
$crypted = Crypt::symmetricEncryptFileContentKeyfile( $this->dataUrl );
$this->assertNotEquals( $this->data, $crypted['encrypted'] );
$this->assertNotEquals( $this->dataUrl, $crypted['encrypted'] );
$decrypt = Crypt::symmetricDecryptFileContent( $crypted['encrypted'], $crypted['key'] );
$this->assertEquals( $this->data, $decrypt );
$this->assertEquals( $this->dataUrl, $decrypt );
}
function testIsEncryptedContent() {
$this->assertFalse( Crypt::isEncryptedContent( $this->data ) );
$this->assertFalse( Crypt::isEncryptedContent( $this->dataUrl ) );
$this->assertFalse( Crypt::isEncryptedContent( $this->legacyEncryptedData ) );
$keyfileContent = Crypt::symmetricEncryptFileContent( $this->data, 'hat' );
$keyfileContent = Crypt::symmetricEncryptFileContent( $this->dataUrl, 'hat' );
$this->assertTrue( Crypt::isEncryptedContent( $keyfileContent ) );
@ -168,14 +224,14 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$this->assertTrue( strlen( $pair1['privateKey'] ) > 1 );
$crypted = Crypt::multiKeyEncrypt( $this->data, array( $pair1['publicKey'] ) );
$crypted = Crypt::multiKeyEncrypt( $this->dataUrl, array( $pair1['publicKey'] ) );
$this->assertNotEquals( $this->data, $crypted['encrypted'] );
$this->assertNotEquals( $this->dataUrl, $crypted['encrypted'] );
$decrypt = Crypt::multiKeyDecrypt( $crypted['encrypted'], $crypted['keys'][0], $pair1['privateKey'] );
$this->assertEquals( $this->data, $decrypt );
$this->assertEquals( $this->dataUrl, $decrypt );
}
@ -185,14 +241,14 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$pair1 = Crypt::createKeypair();
// Encrypt data
$crypted = Crypt::keyEncrypt( $this->data, $pair1['publicKey'] );
$crypted = Crypt::keyEncrypt( $this->dataUrl, $pair1['publicKey'] );
$this->assertNotEquals( $this->data, $crypted );
$this->assertNotEquals( $this->dataUrl, $crypted );
// Decrypt data
$decrypt = Crypt::keyDecrypt( $crypted, $pair1['privateKey'] );
$this->assertEquals( $this->data, $decrypt );
$this->assertEquals( $this->dataUrl, $decrypt );
}
@ -204,7 +260,7 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
$pair1 = Crypt::createKeypair();
// Encrypt plain data, generate keyfile & encrypted file
$cryptedData = Crypt::symmetricEncryptFileContentKeyfile( $this->data );
$cryptedData = Crypt::symmetricEncryptFileContentKeyfile( $this->dataUrl );
// Encrypt keyfile
$cryptedKey = Crypt::keyEncrypt( $cryptedData['key'], $pair1['publicKey'] );
@ -215,7 +271,7 @@ class Test_Crypt extends \PHPUnit_Framework_TestCase {
// Decrypt encrypted file
$decryptData = Crypt::symmetricDecryptFileContent( $cryptedData['encrypted'], $decryptKey );
$this->assertEquals( $this->data, $decryptData );
$this->assertEquals( $this->dataUrl, $decryptData );
}

View file

@ -15,7 +15,8 @@ class Test_Keymanager extends \PHPUnit_Framework_TestCase {
function setUp() {
// set content for encrypting / decrypting in tests
// Set data for use in tests
$this->data = realpath( dirname(__FILE__).'/../lib/crypt.php' );
$this->user = 'admin';
$this->passphrase = 'admin';
$this->view = new \OC_FilesystemView( '' );
@ -39,6 +40,22 @@ class Test_Keymanager extends \PHPUnit_Framework_TestCase {
}
function testSetFileKey() {
# NOTE: This cannot be tested until we are able to break out of the FileSystemView data directory root
// $key = Crypt::symmetricEncryptFileContentKeyfile( $this->data, 'hat' );
//
// $tmpPath = sys_get_temp_dir(). '/' . 'testSetFileKey';
//
// $view = new \OC_FilesystemView( '/tmp/' );
//
// //$view = new \OC_FilesystemView( '/' . $this->user . '/files_encryption/keyfiles' );
//
// Keymanager::setFileKey( $tmpPath, $key['key'], $view );
}
function testGetDecryptedPrivateKey() {
$key = Keymanager::getPrivateKey( $this->user, $this->view );
@ -52,4 +69,6 @@ class Test_Keymanager extends \PHPUnit_Framework_TestCase {
}
}