From 81638436e5be5203628282646944b4490f17f6be Mon Sep 17 00:00:00 2001 From: Kevin Niehage Date: Thu, 29 Dec 2022 16:52:00 +0100 Subject: [PATCH 1/5] introduce wrapped_openssl_seal() and wrapped_openssl_open() to circument RC4 problems with OpenSSL v3 Signed-off-by: Kevin Niehage --- apps/encryption/lib/Crypto/Crypt.php | 166 ++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index efb5a6868b030..62d041aec8caa 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -12,6 +12,7 @@ * @author Roeland Jago Douma * @author Stefan Weiberg * @author Thomas Müller + * @author Kevin Niehage * * @license AGPL-3.0 * @@ -98,6 +99,9 @@ class Crypt { /** @var bool */ private $supportLegacy; + /** @var bool */ + private $wrapRC4 = false; + /** * Use the legacy base64 encoding instead of the more space-efficient binary encoding. */ @@ -116,6 +120,24 @@ public function __construct(ILogger $logger, IUserSession $userSession, IConfig $this->l = $l; $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false); $this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false); + $this->wrapRC4 = $this->checkWrapRC4(); + } + + /** + * checks if RC4 via OpenSSL works as expected + * + * @return bool + */ + public function checkWrapRC4() { + // with OpenSSL v3 we assume that we have to replace the RC4 algo + $result = (OPENSSL_VERSION_NUMBER >= 0x30000000); + + if ($result) { + // maybe someone has re-enabled the legacy support in OpenSSL v3 + $result = (false === openssl_encrypt("test", "rc4", "test", OPENSSL_RAW_DATA, "", $tag, "", 0)); + } + + return $result; } /** @@ -706,7 +728,7 @@ public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) { throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content'); } - if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) { + if ($this->wrapped_openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) { return $plainContent; } else { throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string()); @@ -731,7 +753,7 @@ public function multiKeyEncrypt($plainContent, array $keyFiles) { $shareKeys = []; $mappedShareKeys = []; - if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) { + if ($this->wrapped_openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) { $i = 0; // Ensure each shareKey is labelled with its corresponding key id @@ -749,7 +771,147 @@ public function multiKeyEncrypt($plainContent, array $keyFiles) { } } + /** + * returns the value of $useLegacyBase64Encoding + * + * @return bool + */ public function useLegacyBase64Encoding(): bool { return $this->useLegacyBase64Encoding; } + + /** + * implements RC4 + * + * @param $data + * @param $secret + * @return string + */ + public function rc4($data, $secret) { + // initialize $result + $result = ""; + + // initialize $state + $state = []; + for ($i = 0x00; $i <= 0xFF; $i++) { + $state[$i] = $i; + } + + // mix $secret into $state + $indexA = 0x00; + $indexB = 0x00; + for ($i = 0x00; $i <= 0xFF; $i++) { + $indexB = ($indexB + ord($secret[$indexA]) + $state[$i]) % 0x100; + + $tmp = $state[$i]; + $state[$i] = $state[$indexB]; + $state[$indexB] = $tmp; + + $indexA = ($indexA + 0x01) % strlen($secret); + } + + // decrypt $data with $state + $indexA = 0x00; + $indexB = 0x00; + for ($i = 0x00; $i < strlen($data); $i++) { + $indexA = ($indexA + 0x01) % 0x100; + $indexB = ($state[$indexA] + $indexB) % 0x100; + + $tmp = $state[$indexA]; + $state[$indexA] = $state[$indexB]; + $state[$indexB] = $tmp; + + $result .= chr(ord($data[$i]) ^ $state[($state[$indexA] + $state[$indexB]) % 0x100]); + } + + return $result; + } + + /** + * wraps openssl_open() for cases where RC4 is not supported by OpenSSL v3 + * and replaces it with a custom implementation where necessary + * + * @param $data + * @param $output + * @param $encrypted_key + * @param $private_key + * @param $cipher_algo + * @param $iv + * @return bool + */ + public function wrapped_openssl_open($data, &$output, $encrypted_key, $private_key, $cipher_algo, $iv = null) { + $result = false; + + // check if RC4 is used and if we need to wrap RC4 + if ((0 === strcasecmp($cipher_algo, "rc4")) && $this->wrapRC4) { + // decrypt the intermediate key with RSA + if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) { + // decrypt the file key with the intermediate key + // using our own RC4 implementation + $output = $this->rc4($data, $intermediate); + $result = (strlen($output) === strlen($data)); + } + } else { + // use the default implementation instead + $result = openssl_open($data, $output, $encrypted_key, $private_key, $cipher_algo, $iv); + } + + return $result; + } + + /** + * wraps openssl_seal() for cases where RC4 is not supported by OpenSSL v3 + * and replaces it with a custom implementation where necessary + * + * @param $data + * @param $sealed_data + * @param $encrypted_keys + * @param $public_key + * @param $cipher_algo + * @param $iv + * @return bool|int + */ + public function wrapped_openssl_seal($data, &$sealed_data, &$encrypted_keys, $public_key, $cipher_algo, $iv = null) { + $result = false; + + // check if RC4 is used and if we need to wrap RC4 + if ((0 === strcasecmp($cipher_algo, "rc4")) && $this->wrapRC4) { + // make sure that there is at least one public key to use + if (is_array($public_key) && (1 <= count($public_key))) { + // generate the intermediate key + $intermediate = openssl_random_pseudo_bytes(16, $strong_result); + + // check if we got strong random data + if ($strong_result) { + // encrypt the file key with the intermediate key + // using our own RC4 implementation + $sealed_data = $this->rc4($data, $intermediate); + if (strlen($sealed_data) === strlen($data)) { + // prepare the encrypted keys + $encrypted_keys = []; + + // iterate over the public keys and encrypt the intermediate + // for each of them with RSA + foreach ($public_key as $tmp_key) { + if (openssl_public_encrypt($intermediate, $tmp_output, $tmp_key, OPENSSL_PKCS1_PADDING)) { + $encrypted_keys[] = $tmp_output; + } + } + + // set the result if everything worked fine + if (count($public_key) === count($encrypted_keys)) { + $result = strlen($sealed_data); + } + } + } + } + + } else { + // use the default implementation instead + $result = openssl_seal($data, $sealed_data, $encrypted_keys, $public_key, $cipher_algo, $iv); + } + + return $result; + } + } From deed6393fb47617dbc934ec1e6f39d4d110eb8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 16 Jan 2023 16:45:33 +0100 Subject: [PATCH 2/5] Always wrap rc4, and throws on unknown cipher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/encryption/lib/Crypto/Crypt.php | 45 +++++++--------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index 62d041aec8caa..ba10afd3cd3e4 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -99,9 +99,6 @@ class Crypt { /** @var bool */ private $supportLegacy; - /** @var bool */ - private $wrapRC4 = false; - /** * Use the legacy base64 encoding instead of the more space-efficient binary encoding. */ @@ -120,24 +117,6 @@ public function __construct(ILogger $logger, IUserSession $userSession, IConfig $this->l = $l; $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false); $this->useLegacyBase64Encoding = $this->config->getSystemValueBool('encryption.use_legacy_base64_encoding', false); - $this->wrapRC4 = $this->checkWrapRC4(); - } - - /** - * checks if RC4 via OpenSSL works as expected - * - * @return bool - */ - public function checkWrapRC4() { - // with OpenSSL v3 we assume that we have to replace the RC4 algo - $result = (OPENSSL_VERSION_NUMBER >= 0x30000000); - - if ($result) { - // maybe someone has re-enabled the legacy support in OpenSSL v3 - $result = (false === openssl_encrypt("test", "rc4", "test", OPENSSL_RAW_DATA, "", $tag, "", 0)); - } - - return $result; } /** @@ -803,8 +782,8 @@ public function rc4($data, $secret) { for ($i = 0x00; $i <= 0xFF; $i++) { $indexB = ($indexB + ord($secret[$indexA]) + $state[$i]) % 0x100; - $tmp = $state[$i]; - $state[$i] = $state[$indexB]; + $tmp = $state[$i]; + $state[$i] = $state[$indexB]; $state[$indexB] = $tmp; $indexA = ($indexA + 0x01) % strlen($secret); @@ -817,7 +796,7 @@ public function rc4($data, $secret) { $indexA = ($indexA + 0x01) % 0x100; $indexB = ($state[$indexA] + $indexB) % 0x100; - $tmp = $state[$indexA]; + $tmp = $state[$indexA]; $state[$indexA] = $state[$indexB]; $state[$indexB] = $tmp; @@ -838,12 +817,13 @@ public function rc4($data, $secret) { * @param $cipher_algo * @param $iv * @return bool + * @throws DecryptionFailedException */ public function wrapped_openssl_open($data, &$output, $encrypted_key, $private_key, $cipher_algo, $iv = null) { $result = false; - // check if RC4 is used and if we need to wrap RC4 - if ((0 === strcasecmp($cipher_algo, "rc4")) && $this->wrapRC4) { + // check if RC4 is used + if (strcasecmp($cipher_algo, "rc4") === 0) { // decrypt the intermediate key with RSA if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) { // decrypt the file key with the intermediate key @@ -852,8 +832,7 @@ public function wrapped_openssl_open($data, &$output, $encrypted_key, $private_k $result = (strlen($output) === strlen($data)); } } else { - // use the default implementation instead - $result = openssl_open($data, $output, $encrypted_key, $private_key, $cipher_algo, $iv); + throw new DecryptionFailedException('Unsupported cipher '.$cipher_algo); } return $result; @@ -870,12 +849,13 @@ public function wrapped_openssl_open($data, &$output, $encrypted_key, $private_k * @param $cipher_algo * @param $iv * @return bool|int + * @throws EncryptionFailedException */ public function wrapped_openssl_seal($data, &$sealed_data, &$encrypted_keys, $public_key, $cipher_algo, $iv = null) { $result = false; - // check if RC4 is used and if we need to wrap RC4 - if ((0 === strcasecmp($cipher_algo, "rc4")) && $this->wrapRC4) { + // check if RC4 is used + if (strcasecmp($cipher_algo, "rc4") === 0) { // make sure that there is at least one public key to use if (is_array($public_key) && (1 <= count($public_key))) { // generate the intermediate key @@ -905,13 +885,10 @@ public function wrapped_openssl_seal($data, &$sealed_data, &$encrypted_keys, $pu } } } - } else { - // use the default implementation instead - $result = openssl_seal($data, $sealed_data, $encrypted_keys, $public_key, $cipher_algo, $iv); + throw new EncryptionFailedException('Unsupported cipher '.$cipher_algo); } return $result; } - } From bd626e36933d31be3f6a4ba4fdca74719cb9f71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 16 Jan 2023 17:08:50 +0100 Subject: [PATCH 3/5] Strong type custom openssl_seal implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/encryption/lib/Crypto/Crypt.php | 40 +++++++++------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index ba10afd3cd3e4..a455e86fcbd52 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -518,12 +518,9 @@ public function symmetricDecryptFileContent($keyFileContents, $passPhrase, $ciph /** * check for valid signature * - * @param string $data - * @param string $passPhrase - * @param string $expectedSignature * @throws GenericEncryptionException */ - private function checkSignature($data, $passPhrase, $expectedSignature) { + private function checkSignature(string $data, string $passPhrase, string $expectedSignature): void { $enforceSignature = !$this->config->getSystemValueBool('encryption_skip_signature_check', false); $signature = $this->createSignature($data, $passPhrase); @@ -696,9 +693,9 @@ public function generateFileKey() { } /** - * @param $encKeyFile - * @param $shareKey - * @param $privateKey + * @param string $encKeyFile + * @param string $shareKey + * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $privateKey * @return string * @throws MultiKeyDecryptException */ @@ -707,7 +704,8 @@ public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) { throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content'); } - if ($this->wrapped_openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) { + $plainContent = ''; + if ($this->opensslOpen($encKeyFile, $plainContent, $shareKey, $privateKey, 'RC4')) { return $plainContent; } else { throw new MultiKeyDecryptException('multikeydecrypt with share key failed:' . openssl_error_string()); @@ -732,7 +730,7 @@ public function multiKeyEncrypt($plainContent, array $keyFiles) { $shareKeys = []; $mappedShareKeys = []; - if ($this->wrapped_openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) { + if ($this->opensslSeal($plainContent, $sealed, $shareKeys, $keyFiles, 'RC4')) { $i = 0; // Ensure each shareKey is labelled with its corresponding key id @@ -810,16 +808,10 @@ public function rc4($data, $secret) { * wraps openssl_open() for cases where RC4 is not supported by OpenSSL v3 * and replaces it with a custom implementation where necessary * - * @param $data - * @param $output - * @param $encrypted_key - * @param $private_key - * @param $cipher_algo - * @param $iv - * @return bool + * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key * @throws DecryptionFailedException */ - public function wrapped_openssl_open($data, &$output, $encrypted_key, $private_key, $cipher_algo, $iv = null) { + public function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool { $result = false; // check if RC4 is used @@ -839,25 +831,17 @@ public function wrapped_openssl_open($data, &$output, $encrypted_key, $private_k } /** - * wraps openssl_seal() for cases where RC4 is not supported by OpenSSL v3 - * and replaces it with a custom implementation where necessary + * Custom implementation of openssl_seal() * - * @param $data - * @param $sealed_data - * @param $encrypted_keys - * @param $public_key - * @param $cipher_algo - * @param $iv - * @return bool|int * @throws EncryptionFailedException */ - public function wrapped_openssl_seal($data, &$sealed_data, &$encrypted_keys, $public_key, $cipher_algo, $iv = null) { + public function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false { $result = false; // check if RC4 is used if (strcasecmp($cipher_algo, "rc4") === 0) { // make sure that there is at least one public key to use - if (is_array($public_key) && (1 <= count($public_key))) { + if (count($public_key) >= 1) { // generate the intermediate key $intermediate = openssl_random_pseudo_bytes(16, $strong_result); From 71482576ad9ab0a2231e792d4a30605651fefb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 21 Feb 2023 09:47:03 +0100 Subject: [PATCH 4/5] Move to phpseclib implementation of RC4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- apps/encryption/lib/Crypto/Crypt.php | 58 ++++++++-------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index a455e86fcbd52..2f188eec76078 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -41,6 +41,7 @@ use OCP\IL10N; use OCP\ILogger; use OCP\IUserSession; +use phpseclib\Crypt\RC4; /** * Class Crypt provides the encryption implementation of the default Nextcloud @@ -758,50 +759,23 @@ public function useLegacyBase64Encoding(): bool { } /** - * implements RC4 - * - * @param $data - * @param $secret - * @return string + * Uses phpseclib RC4 implementation */ - public function rc4($data, $secret) { - // initialize $result - $result = ""; - - // initialize $state - $state = []; - for ($i = 0x00; $i <= 0xFF; $i++) { - $state[$i] = $i; - } - - // mix $secret into $state - $indexA = 0x00; - $indexB = 0x00; - for ($i = 0x00; $i <= 0xFF; $i++) { - $indexB = ($indexB + ord($secret[$indexA]) + $state[$i]) % 0x100; - - $tmp = $state[$i]; - $state[$i] = $state[$indexB]; - $state[$indexB] = $tmp; - - $indexA = ($indexA + 0x01) % strlen($secret); - } - - // decrypt $data with $state - $indexA = 0x00; - $indexB = 0x00; - for ($i = 0x00; $i < strlen($data); $i++) { - $indexA = ($indexA + 0x01) % 0x100; - $indexB = ($state[$indexA] + $indexB) % 0x100; + protected function rc4Decrypt(string $data, string $secret): string { + $rc4 = new RC4(); + $rc4->setKey($secret); - $tmp = $state[$indexA]; - $state[$indexA] = $state[$indexB]; - $state[$indexB] = $tmp; + return $rc4->decrypt($data); + } - $result .= chr(ord($data[$i]) ^ $state[($state[$indexA] + $state[$indexB]) % 0x100]); - } + /** + * Uses phpseclib RC4 implementation + */ + protected function rc4Encrypt(string $data, string $secret): string { + $rc4 = new RC4(); + $rc4->setKey($secret); - return $result; + return $rc4->encrypt($data); } /** @@ -820,7 +794,7 @@ public function opensslOpen(string $data, string &$output, string $encrypted_key if (openssl_private_decrypt($encrypted_key, $intermediate, $private_key, OPENSSL_PKCS1_PADDING)) { // decrypt the file key with the intermediate key // using our own RC4 implementation - $output = $this->rc4($data, $intermediate); + $output = $this->rc4Decrypt($data, $intermediate); $result = (strlen($output) === strlen($data)); } } else { @@ -849,7 +823,7 @@ public function opensslSeal(string $data, string &$sealed_data, array &$encrypte if ($strong_result) { // encrypt the file key with the intermediate key // using our own RC4 implementation - $sealed_data = $this->rc4($data, $intermediate); + $sealed_data = $this->rc4Encrypt($data, $intermediate); if (strlen($sealed_data) === strlen($data)) { // prepare the encrypted keys $encrypted_keys = []; From f2912ce8bcd4316b587e249d3deb94c461caddaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Tue, 21 Feb 2023 11:20:59 +0100 Subject: [PATCH 5/5] Set functions as private to be able to refactor later MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also a few comment fixes Signed-off-by: Côme Chilliet --- apps/encryption/lib/Crypto/Crypt.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php index 2f188eec76078..fe9813a6cfa31 100644 --- a/apps/encryption/lib/Crypto/Crypt.php +++ b/apps/encryption/lib/Crypto/Crypt.php @@ -6,13 +6,14 @@ * @author Björn Schießle * @author Christoph Wurst * @author Clark Tomlinson + * @author Côme Chilliet * @author Joas Schilling + * @author Kevin Niehage * @author Lukas Reschke * @author Morris Jobke * @author Roeland Jago Douma * @author Stefan Weiberg * @author Thomas Müller - * @author Kevin Niehage * * @license AGPL-3.0 * @@ -761,8 +762,9 @@ public function useLegacyBase64Encoding(): bool { /** * Uses phpseclib RC4 implementation */ - protected function rc4Decrypt(string $data, string $secret): string { + private function rc4Decrypt(string $data, string $secret): string { $rc4 = new RC4(); + /** @psalm-suppress InternalMethod */ $rc4->setKey($secret); return $rc4->decrypt($data); @@ -771,21 +773,21 @@ protected function rc4Decrypt(string $data, string $secret): string { /** * Uses phpseclib RC4 implementation */ - protected function rc4Encrypt(string $data, string $secret): string { + private function rc4Encrypt(string $data, string $secret): string { $rc4 = new RC4(); + /** @psalm-suppress InternalMethod */ $rc4->setKey($secret); return $rc4->encrypt($data); } /** - * wraps openssl_open() for cases where RC4 is not supported by OpenSSL v3 - * and replaces it with a custom implementation where necessary + * Custom implementation of openssl_open() * * @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|array|string $private_key * @throws DecryptionFailedException */ - public function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool { + private function opensslOpen(string $data, string &$output, string $encrypted_key, $private_key, string $cipher_algo): bool { $result = false; // check if RC4 is used @@ -809,7 +811,7 @@ public function opensslOpen(string $data, string &$output, string $encrypted_key * * @throws EncryptionFailedException */ - public function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false { + private function opensslSeal(string $data, string &$sealed_data, array &$encrypted_keys, array $public_key, string $cipher_algo): int|false { $result = false; // check if RC4 is used