Merge pull request #32110 from plumbeo/binary-encoding-4

Save encrypted files in binary format
pull/32268/head
Vincent Petry 2 years ago committed by GitHub
commit fe24091ffb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -77,6 +77,9 @@ class Crypt {
public const HEADER_START = 'HBEGIN';
public const HEADER_END = 'HEND';
// default encoding format, old Nextcloud versions used base64
public const BINARY_ENCODING_FORMAT = 'binary';
/** @var ILogger */
private $logger;
@ -95,6 +98,11 @@ class Crypt {
/** @var bool */
private $supportLegacy;
/**
* Use the legacy base64 encoding instead of the more space-efficient binary encoding.
*/
private bool $useLegacyBase64Encoding;
/**
* @param ILogger $logger
* @param IUserSession $userSession
@ -107,6 +115,7 @@ class Crypt {
$this->config = $config;
$this->l = $l;
$this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false);
$this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false);
}
/**
@ -215,12 +224,15 @@ class Crypt {
throw new \InvalidArgumentException('key format "' . $keyFormat . '" is not supported');
}
$cipher = $this->getCipher();
$header = self::HEADER_START
. ':cipher:' . $cipher
. ':keyFormat:' . $keyFormat
. ':' . self::HEADER_END;
. ':cipher:' . $this->getCipher()
. ':keyFormat:' . $keyFormat;
if ($this->useLegacyBase64Encoding !== true) {
$header .= ':encoding:' . self::BINARY_ENCODING_FORMAT;
}
$header .= ':' . self::HEADER_END;
return $header;
}
@ -234,10 +246,11 @@ class Crypt {
* @throws EncryptionFailedException
*/
private function encrypt($plainContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
$options = $this->useLegacyBase64Encoding ? 0 : OPENSSL_RAW_DATA;
$encryptedContent = openssl_encrypt($plainContent,
$cipher,
$passPhrase,
0,
$options,
$iv);
if (!$encryptedContent) {
@ -424,6 +437,8 @@ class Crypt {
$password = $this->generatePasswordHash($password, $cipher, $uid);
}
$binaryEncoding = isset($header['encoding']) && $header['encoding'] === self::BINARY_ENCODING_FORMAT;
// If we found a header we need to remove it from the key we want to decrypt
if (!empty($header)) {
$privateKey = substr($privateKey,
@ -435,7 +450,9 @@ class Crypt {
$privateKey,
$password,
$cipher,
0
0,
0,
$binaryEncoding
);
if ($this->isValidPrivateKey($plainKey) === false) {
@ -470,10 +487,11 @@ class Crypt {
* @param string $cipher
* @param int $version
* @param int|string $position
* @param boolean $binaryEncoding
* @return string
* @throws DecryptionFailedException
*/
public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0) {
public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $cipher = self::DEFAULT_CIPHER, $version = 0, $position = 0, bool $binaryEncoding = false) {
if ($keyFileContents == '') {
return '';
}
@ -493,7 +511,8 @@ class Crypt {
return $this->decrypt($catFile['encrypted'],
$catFile['iv'],
$passPhrase,
$cipher);
$cipher,
$binaryEncoding);
}
/**
@ -610,14 +629,16 @@ class Crypt {
* @param string $iv
* @param string $passPhrase
* @param string $cipher
* @param boolean $binaryEncoding
* @return string
* @throws DecryptionFailedException
*/
private function decrypt($encryptedContent, $iv, $passPhrase = '', $cipher = self::DEFAULT_CIPHER) {
private function decrypt(string $encryptedContent, string $iv, string $passPhrase = '', string $cipher = self::DEFAULT_CIPHER, bool $binaryEncoding = false): string {
$options = $binaryEncoding === true ? OPENSSL_RAW_DATA : 0;
$plainContent = openssl_decrypt($encryptedContent,
$cipher,
$passPhrase,
0,
$options,
$iv);
if ($plainContent) {
@ -728,4 +749,8 @@ class Crypt {
throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string());
}
}
public function useLegacyBase64Encoding(): bool {
return $this->useLegacyBase64Encoding;
}
}

@ -103,11 +103,7 @@ 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;
private bool $useLegacyBase64Encoding = false;
/** @var int Current version of the file */
private $version = 0;
@ -184,6 +180,11 @@ class Encryption implements IEncryptionModule {
$this->user = $user;
$this->isWriteOperation = false;
$this->writeCache = '';
$this->useLegacyBase64Encoding = true;
if (isset($header['encoding'])) {
$this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT;
}
if ($this->session->isReady() === false) {
// if the master key is enabled we can initialize encryption
@ -229,6 +230,7 @@ class Encryption implements IEncryptionModule {
if ($this->isWriteOperation) {
$this->cipher = $this->crypt->getCipher();
$this->useLegacyBase64Encoding = $this->crypt->useLegacyBase64Encoding();
} elseif (isset($header['cipher'])) {
$this->cipher = $header['cipher'];
} else {
@ -237,7 +239,13 @@ class Encryption implements IEncryptionModule {
$this->cipher = $this->crypt->getLegacyCipher();
}
return ['cipher' => $this->cipher, 'signed' => 'true'];
$result = ['cipher' => $this->cipher, 'signed' => 'true'];
if ($this->useLegacyBase64Encoding !== true) {
$result['encoding'] = Crypt::BINARY_ENCODING_FORMAT;
}
return $result;
}
/**
@ -325,8 +333,8 @@ class Encryption implements IEncryptionModule {
$remainingLength = strlen($data);
// If data remaining to be written is less than the
// size of 1 6126 byte block
if ($remainingLength < $this->unencryptedBlockSizeSigned) {
// size of 1 unencrypted block
if ($remainingLength < $this->getUnencryptedBlockSize(true)) {
// Set writeCache to contents of $data
// The writeCache will be carried over to the
@ -343,14 +351,14 @@ class Encryption implements IEncryptionModule {
} else {
// Read the chunk from the start of $data
$chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
$chunk = substr($data, 0, $this->getUnencryptedBlockSize(true));
$encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $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, $this->unencryptedBlockSizeSigned);
$data = substr($data, $this->getUnencryptedBlockSize(true));
}
}
@ -374,7 +382,7 @@ class Encryption implements IEncryptionModule {
throw new DecryptionFailedException($msg, $hint);
}
return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position, !$this->useLegacyBase64Encoding);
}
/**
@ -462,15 +470,27 @@ class Encryption implements IEncryptionModule {
* get size of the unencrypted payload per block.
* Nextcloud read/write files with a block size of 8192 byte
*
* Encrypted blocks have a 22-byte IV and 2 bytes of padding, encrypted and
* signed blocks have also a 71-byte signature and 1 more byte of padding,
* resulting respectively in:
*
* 8192 - 22 - 2 = 8168 bytes in each unsigned unencrypted block
* 8192 - 22 - 2 - 71 - 1 = 8096 bytes in each signed unencrypted block
*
* Legacy base64 encoding then reduces the available size by a 3/4 factor:
*
* 8168 * (3/4) = 6126 bytes in each base64-encoded unsigned unencrypted block
* 8096 * (3/4) = 6072 bytes in each base64-encoded signed unencrypted block
*
* @param bool $signed
* @return int
*/
public function getUnencryptedBlockSize($signed = false) {
if ($signed === false) {
return $this->unencryptedBlockSize;
if ($this->useLegacyBase64Encoding) {
return $signed ? 6072 : 6126;
} else {
return $signed ? 8096 : 8168;
}
return $this->unencryptedBlockSizeSigned;
}
/**

@ -139,9 +139,9 @@ class CryptTest extends TestCase {
*/
public function dataTestGenerateHeader() {
return [
[null, 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:HEND'],
['password', 'HBEGIN:cipher:AES-128-CFB:keyFormat:password:HEND'],
['hash', 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:HEND']
[null, 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:encoding:binary:HEND'],
['password', 'HBEGIN:cipher:AES-128-CFB:keyFormat:password:encoding:binary:HEND'],
['hash', 'HBEGIN:cipher:AES-128-CFB:keyFormat:hash:encoding:binary:HEND']
];
}
@ -305,15 +305,17 @@ class CryptTest extends TestCase {
* test parseHeader()
*/
public function testParseHeader() {
$header = 'HBEGIN:foo:bar:cipher:AES-256-CFB:HEND';
$header = 'HBEGIN:foo:bar:cipher:AES-256-CFB:encoding:binary:HEND';
$result = self::invokePrivate($this->crypt, 'parseHeader', [$header]);
$this->assertTrue(is_array($result));
$this->assertSame(2, count($result));
$this->assertSame(3, count($result));
$this->assertArrayHasKey('foo', $result);
$this->assertArrayHasKey('cipher', $result);
$this->assertArrayHasKey('encoding', $result);
$this->assertSame('bar', $result['foo']);
$this->assertSame('AES-256-CFB', $result['cipher']);
$this->assertSame('binary', $result['encoding']);
}
/**
@ -324,18 +326,20 @@ class CryptTest extends TestCase {
public function testEncrypt() {
$decrypted = 'content';
$password = 'password';
$cipher = 'AES-256-CTR';
$iv = self::invokePrivate($this->crypt, 'generateIv');
$this->assertTrue(is_string($iv));
$this->assertSame(16, strlen($iv));
$result = self::invokePrivate($this->crypt, 'encrypt', [$decrypted, $iv, $password]);
$result = self::invokePrivate($this->crypt, 'encrypt', [$decrypted, $iv, $password, $cipher]);
$this->assertTrue(is_string($result));
return [
'password' => $password,
'iv' => $iv,
'cipher' => $cipher,
'encrypted' => $result,
'decrypted' => $decrypted];
}
@ -349,7 +353,7 @@ class CryptTest extends TestCase {
$result = self::invokePrivate(
$this->crypt,
'decrypt',
[$data['encrypted'], $data['iv'], $data['password']]);
[$data['encrypted'], $data['iv'], $data['password'], $data['cipher'], true]);
$this->assertSame($data['decrypted'], $result);
}
@ -391,8 +395,9 @@ class CryptTest extends TestCase {
*/
public function testDecryptPrivateKey($header, $privateKey, $expectedCipher, $isValidKey, $expected) {
$this->config->method('getSystemValueBool')
->with('encryption.legacy_format_support', false)
->willReturn(true);
->withConsecutive(['encryption.legacy_format_support', false],
['encryption.use_legacy_base64_encoding', false])
->willReturnOnConsecutiveCalls(true, false);
/** @var \OCA\Encryption\Crypto\Crypt | \PHPUnit\Framework\MockObject\MockObject $crypt */
$crypt = $this->getMockBuilder(Crypt::class)

@ -1807,6 +1807,15 @@ $CONFIG = [
*/
'cipher' => 'AES-256-CTR',
/**
* Use the legacy base64 format for encrypted files instead of the more space-efficient
* binary format. The option affects only newly written files, existing encrypted files
* will not be touched and will remain readable whether they use the new format or not.
*
* Defaults to ``false``
*/
'encryption.use_legacy_base64_encoding' => false,
/**
* The minimum Nextcloud desktop client version that will be allowed to sync with
* this server instance. All connections made from earlier clients will be denied

@ -259,7 +259,6 @@ class Encryption extends Wrapper {
$this->cache = '';
$this->writeFlag = false;
$this->fileUpdated = false;
$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
if (
$mode === 'w'
@ -284,6 +283,7 @@ class Encryption extends Wrapper {
$accessList = $this->file->getAccessList($sharePath);
}
$this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $mode, $this->header, $accessList);
$this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize($this->signed);
if (
$mode === 'w'

Loading…
Cancel
Save