From 56bb4167981bdd2a2c669a130a075feaacbd401c Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 09:33:43 -0500 Subject: [PATCH 01/24] chore: add typing to IEncryptionModule Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 56 +++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 0bf0ef3476beb..89327b2b18088 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -1,5 +1,7 @@ end") * - * @return string remained data which should be written to the file in case - * of a write operation + * @return string|null remained data which should be written to the file in case + * of a write operation, or null * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function end($path, $position); + public function end(string $path, string $position): ?string; /** * encrypt data * * @param string $data you want to encrypt - * @param string $position position of the block we want to encrypt (starts with '0') + * @param string|int $position position of the block we want to encrypt (starts with '0') * - * @return mixed encrypted data + * @return string encrypted data * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function encrypt($data, $position); + public function encrypt(string $data, string|int $position): string; /** * decrypt data * * @param string $data you want to decrypt - * @param int|string $position position of the block we want to decrypt + * @param string|int $position position of the block we want to decrypt * - * @return mixed decrypted data + * @return string decrypted data * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function decrypt($data, $position); + public function decrypt(string $data, string|int $position): string; /** * update encrypted file, e.g. give additional users access to the file @@ -96,19 +98,19 @@ public function decrypt($data, $position); * @param string $path path to the file which should be updated * @param string $uid of the user who performs the operation * @param array $accessList who has access to the file contains the key 'users' and 'public' - * @return boolean + * @return bool * @since 8.1.0 */ - public function update($path, $uid, array $accessList); + public function update(string $path, string $uid, array $accessList): bool; /** * should the file be encrypted or not * * @param string $path - * @return boolean + * @return bool * @since 8.1.0 */ - public function shouldEncrypt($path); + public function shouldEncrypt(string $path): bool; /** * get size of the unencrypted payload per block. @@ -118,7 +120,7 @@ public function shouldEncrypt($path); * @return int * @since 8.1.0 optional parameter $signed was added in 9.0.0 */ - public function getUnencryptedBlockSize($signed = false); + public function getUnencryptedBlockSize(bool $signed = false): int; /** * check if the encryption module is able to read the file, @@ -126,10 +128,10 @@ public function getUnencryptedBlockSize($signed = false); * * @param string $path * @param string $uid user for whom we want to check if they can read the file - * @return boolean + * @return bool * @since 8.1.0 */ - public function isReadable($path, $uid); + public function isReadable(string $path, string $uid): bool; /** * Initial encryption of all files @@ -138,18 +140,18 @@ public function isReadable($path, $uid); * @param OutputInterface $output write some status information to the terminal during encryption * @since 8.2.0 */ - public function encryptAll(InputInterface $input, OutputInterface $output); + public function encryptAll(InputInterface $input, OutputInterface $output): void; /** * prepare encryption module to decrypt all files * * @param InputInterface $input * @param OutputInterface $output write some status information to the terminal during encryption - * @param $user (optional) for which the files should be decrypted, default = all users + * @param string $user (optional) for which the files should be decrypted, default = all users * @return bool return false on failure or if it isn't supported by the module * @since 8.2.0 */ - public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = ''); + public function prepareDecryptAll(InputInterface $input, OutputInterface $output, string $user = ''): bool; /** * Check if the module is ready to be used by that specific user. @@ -158,10 +160,10 @@ public function prepareDecryptAll(InputInterface $input, OutputInterface $output * cause issues during operations. * * @param string $user - * @return boolean + * @return bool * @since 9.1.0 */ - public function isReadyForUser($user); + public function isReadyForUser(string $user): bool; /** * Does the encryption module needs a detailed list of users with access to the file? @@ -171,5 +173,5 @@ public function isReadyForUser($user); * @since 13.0.0 * @return bool */ - public function needDetailedAccessList(); + public function needDetailedAccessList(): bool; } From 87652053ab27ec89377098287e2d70dcb11221cd Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 10:29:08 -0500 Subject: [PATCH 02/24] chore: add type hints throughout IEncryptionModule implementation Signed-off-by: Josh --- apps/encryption/lib/Crypto/Encryption.php | 149 ++++++++++------------ 1 file changed, 69 insertions(+), 80 deletions(-) diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index daeb9859b41cc..dd210ccaf87cb 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -1,5 +1,7 @@ path = $this->getPathToRealFile($path); $this->accessList = $accessList; $this->user = $user; @@ -111,7 +95,6 @@ public function begin($path, $user, $mode, array $header, array $accessList) { $this->writeCache = ''; $this->useLegacyBase64Encoding = true; - if (isset($header['encoding'])) { $this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT; } @@ -124,7 +107,7 @@ public function begin($path, $user, $mode, array $header, array $accessList) { } } - /* If useLegacyFileKey is not specified in header, auto-detect, to be safe */ + // If useLegacyFileKey is not specified in header, auto-detect, to be safe $useLegacyFileKey = (($header['useLegacyFileKey'] ?? '') == 'false' ? false : null); $this->fileKey = $this->keyManager->getFileKey($this->path, $useLegacyFileKey, $this->session->decryptAllModeActivated()); @@ -190,7 +173,7 @@ public function begin($path, $user, $mode, array $header, array $accessList) { * @throws \Exception * @throws MultiKeyEncryptException */ - public function end($path, $position = '0') { + public function end(string $path, string $position = '0'): string { $result = ''; if ($this->isWriteOperation) { // in case of a part file we remember the new signature versions @@ -239,22 +222,19 @@ public function end($path, $position = '0') { return $result ?: ''; } - - /** * encrypt data * * @param string $data you want to encrypt - * @param int $position + * @param string $position * @return string encrypted data */ - public function encrypt($data, $position = 0) { + public function encrypt(string $data, string $position = '0'): string { // If extra data is left over from the last round, make sure it // is integrated into the next block if ($this->writeCache) { // Concat writeCache to start of $data $data = $this->writeCache . $data; - // Clear the write cache, ready for reuse - it has been // flushed and its old contents processed $this->writeCache = ''; @@ -286,7 +266,7 @@ public function encrypt($data, $position = 0) { // Read the chunk from the start of $data $chunk = substr($data, 0, $this->getUnencryptedBlockSize(true)); - $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, (string)$position); + $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 @@ -302,11 +282,11 @@ public function encrypt($data, $position = 0) { * decrypt data * * @param string $data you want to decrypt - * @param int|string $position + * @param string $position * @return string decrypted data * @throws DecryptionFailedException */ - public function decrypt($data, $position = 0) { + public function decrypt(string $data, string $position = '0'): string { if (empty($this->fileKey)) { $msg = 'Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'; $hint = $this->l->t('Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'); @@ -315,7 +295,14 @@ public function decrypt($data, $position = 0) { throw new DecryptionFailedException($msg, $hint); } - return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position, !$this->useLegacyBase64Encoding); + return $this->crypt->symmetricDecryptFileContent( + $data, + $this->fileKey, + $this->cipher, + $this->version, + $position, + !$this->useLegacyBase64Encoding + ); } /** @@ -326,7 +313,7 @@ public function decrypt($data, $position = 0) { * @param array $accessList who has access to the file contains the key 'users' and 'public' * @return bool */ - public function update($path, $uid, array $accessList) { + public function update(string $path, string $uid, array $accessList): bool { if (empty($accessList)) { if (isset(self::$rememberVersion[$path])) { $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View()); @@ -374,9 +361,9 @@ public function update($path, $uid, array $accessList) { * should the file be encrypted or not * * @param string $path - * @return boolean + * @return bool */ - public function shouldEncrypt($path) { + public function shouldEncrypt(string $path): bool { if ($this->util->shouldEncryptHomeStorage() === false) { $storage = $this->util->getStorage($path); if ($storage && $storage->instanceOfStorage('\OCP\Files\IHomeStorage')) { @@ -420,7 +407,7 @@ public function shouldEncrypt($path) { * @param bool $signed * @return int */ - public function getUnencryptedBlockSize($signed = false) { + public function getUnencryptedBlockSize(bool $signed = false): int { if ($this->useLegacyBase64Encoding) { return $signed ? 6072 : 6126; } else { @@ -437,7 +424,7 @@ public function getUnencryptedBlockSize($signed = false) { * @return bool * @throws DecryptionFailedException */ - public function isReadable($path, $uid) { + public function isReadable(string $path, string $uid): bool { $fileKey = $this->keyManager->getFileKey($path, null); if (empty($fileKey)) { $owner = $this->util->getOwner($path); @@ -464,7 +451,7 @@ public function isReadable($path, $uid) { * @param InputInterface $input * @param OutputInterface $output write some status information to the terminal during encryption */ - public function encryptAll(InputInterface $input, OutputInterface $output) { + public function encryptAll(InputInterface $input, OutputInterface $output): void { $this->encryptAll->encryptAll($input, $output); } @@ -476,16 +463,44 @@ public function encryptAll(InputInterface $input, OutputInterface $output) { * @param string $user * @return bool */ - public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') { + public function prepareDecryptAll(InputInterface $input, OutputInterface $output, string $user = ''): bool { return $this->decryptAll->prepare($input, $output, $user); } + /** + * Check if the module is ready to be used by that specific user. + * In case a module is not ready - because e.g. key pairs have not been generated + * upon login this method can return false before any operation starts and might + * cause issues during operations. + * + * @param string $user + * @return bool + * @since 9.1.0 + */ + public function isReadyForUser(string $user): bool { + if ($this->util->isMasterKeyEnabled()) { + return true; + } + return $this->keyManager->userHasKeys($user); + } + + /** + * Does the encryption module needs a detailed list of users with access to the file? + * For example if the encryption module uses per-user encryption keys and needs to know + * the users with access to the file to encrypt/decrypt it. + * + * @since 13.0.0 + * @return bool + */ + public function needDetailedAccessList(): bool { + return !$this->util->isMasterKeyEnabled(); + } /** * @param string $path * @return string */ - protected function getPathToRealFile($path) { + protected function getPathToRealFile(string $path): string { $realPath = $path; $parts = explode('/', $path); if ($parts[2] === 'files_versions') { @@ -504,7 +519,7 @@ protected function getPathToRealFile($path) { * @param string $path * @return string */ - protected function stripPartFileExtension($path) { + protected function stripPartFileExtension(string $path): string { if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { $pos = strrpos($path, '.', -6); $path = substr($path, 0, $pos); @@ -519,36 +534,10 @@ protected function stripPartFileExtension($path) { * @param string $path * @return string */ - protected function getOwner($path) { + protected function getOwner(string $path): string { if (!isset($this->owner[$path])) { $this->owner[$path] = $this->util->getOwner($path); } return $this->owner[$path]; } - - /** - * Check if the module is ready to be used by that specific user. - * In case a module is not ready - because e.g. key pairs have not been generated - * upon login this method can return false before any operation starts and might - * cause issues during operations. - * - * @param string $user - * @return boolean - * @since 9.1.0 - */ - public function isReadyForUser($user) { - if ($this->util->isMasterKeyEnabled()) { - return true; - } - return $this->keyManager->userHasKeys($user); - } - - /** - * We only need a detailed access list if the master key is not enabled - * - * @return bool - */ - public function needDetailedAccessList() { - return !$this->util->isMasterKeyEnabled(); - } } From 86a6c35c51830eb43b6cddce40260eae7c771503 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 10:31:52 -0500 Subject: [PATCH 03/24] chore: correct $position typing in IEncryptionModule Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 89327b2b18088..e519c0a29f6de 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -64,33 +64,33 @@ public function begin(string $path, ?string $user, string $mode, array $header, * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function end(string $path, string $position): ?string; + public function end(string $path, string $position = '0'): ?string; /** * encrypt data * * @param string $data you want to encrypt - * @param string|int $position position of the block we want to encrypt (starts with '0') + * @param string $position position of the block we want to encrypt (starts with '0') * * @return string encrypted data * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function encrypt(string $data, string|int $position): string; + public function encrypt(string $data, string $position = '0'): string; /** * decrypt data * * @param string $data you want to decrypt - * @param string|int $position position of the block we want to decrypt + * @param string $position position of the block we want to decrypt * * @return string decrypted data * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function decrypt(string $data, string|int $position): string; + public function decrypt(string $data, string $position = '0'): string; /** * update encrypted file, e.g. give additional users access to the file From 319754ddcf9216255e95b32c21a05843d539a905 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 10:56:53 -0500 Subject: [PATCH 04/24] chore: add exceptions to IEncryptionModule contract docs Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index e519c0a29f6de..270a1b19b7a27 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -58,13 +58,17 @@ public function begin(string $path, ?string $user, string $mode, array $header, * @param string $path to the file * @param string $position id of the last block (looks like "end") * - * @return string|null remained data which should be written to the file in case - * of a write operation, or null + * @return string remained data which should be written to the file in case + * of a write operation + * + * @throws PublicKeyMissingException + * @throws \Exception + * @throws MultiKeyEncryptException * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function end(string $path, string $position = '0'): ?string; + public function end(string $path, string $position = '0'): string; /** * encrypt data @@ -87,6 +91,8 @@ public function encrypt(string $data, string $position = '0'): string; * * @return string decrypted data * + * @throws DecryptionFailedException + * * @since 8.1.0 * @since 9.0.0 parameter $position added */ From 82709d8cd055aa1a5b73483ecdeeb9fa666b9646 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 11:01:11 -0500 Subject: [PATCH 05/24] chore: add simple docblock to interface IEncryptionModule Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 270a1b19b7a27..836d08349b958 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -13,7 +13,11 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Interface IEncryptionModule + * Defines the contract for a pluggable storage encryption module. + * + * Implementations provide algorithms and key management to transparently + * encrypt and decrypt user file content, supporting block operations, + * file sharing, and migration. * * @since 8.1.0 */ From c26f27538c5db4e6bb889d789d22a32e955eace0 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 11:19:35 -0500 Subject: [PATCH 06/24] chore: drop duplicated API docblocks from Encryption implementation They're now all well defined in the interface (and mostly already were). The rare implementation specific bits retained. Signed-off-by: Josh --- apps/encryption/lib/Crypto/Encryption.php | 133 +++------------------- 1 file changed, 15 insertions(+), 118 deletions(-) diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index dd210ccaf87cb..3f53ae32becb5 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -23,6 +23,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Default file content encryption module. + * + * Implements block-based encryption, decryption, key management, + * and access control for user data storage. + * + * @see \OCP\Encryption\IEncryptionModule for detailed method documentation and contract. + */ class Encryption implements IEncryptionModule { public const ID = 'OC_DEFAULT_MODULE'; public const DISPLAY_NAME = 'Default encryption module'; @@ -56,37 +64,14 @@ public function __construct( $this->useMasterPassword = $this->util->isMasterKeyEnabled(); } - /** - * @return string defining the technical unique id - */ public function getId(): string { return self::ID; } - /** - * In comparison to getKey() this function returns a human readable (maybe translated) name - * - * @return string - */ public function getDisplayName(): string { return self::DISPLAY_NAME; } - /** - * start receiving chunks from a file. This is the place where you can - * perform some initial step before starting encrypting/decrypting the - * chunks - * - * @param string $path to the file - * @param string|null $user who read/write the file (null for public access) - * @param string $mode php stream open mode - * @param array $header contains the header data read from the file - * @param array $accessList who has access to the file contains the key 'users' and 'public' - * - * @return array $header contain data as key-value pairs which should be - * written to the header, in case of a write operation - * or if no additional data is needed return a empty array - */ public function begin(string $path, ?string $user, string $mode, array $header, array $accessList): array { $this->path = $this->getPathToRealFile($path); $this->accessList = $accessList; @@ -160,19 +145,6 @@ public function begin(string $path, ?string $user, string $mode, array $header, return $result; } - /** - * last chunk received. This is the place where you can perform some final - * operation and return some remaining data if something is left in your - * buffer. - * - * @param string $path to the file - * @param string $position - * @return string remained data which should be written to the file in case - * of a write operation - * @throws PublicKeyMissingException - * @throws \Exception - * @throws MultiKeyEncryptException - */ public function end(string $path, string $position = '0'): string { $result = ''; if ($this->isWriteOperation) { @@ -222,13 +194,6 @@ public function end(string $path, string $position = '0'): string { return $result ?: ''; } - /** - * encrypt data - * - * @param string $data you want to encrypt - * @param string $position - * @return string encrypted data - */ public function encrypt(string $data, string $position = '0'): string { // If extra data is left over from the last round, make sure it // is integrated into the next block @@ -278,14 +243,6 @@ public function encrypt(string $data, string $position = '0'): string { return $encrypted; } - /** - * decrypt data - * - * @param string $data you want to decrypt - * @param string $position - * @return string decrypted data - * @throws DecryptionFailedException - */ public function decrypt(string $data, string $position = '0'): string { if (empty($this->fileKey)) { $msg = 'Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'; @@ -305,14 +262,6 @@ public function decrypt(string $data, string $position = '0'): string { ); } - /** - * update encrypted file, e.g. give additional users access to the file - * - * @param string $path path to the file which should be updated - * @param string $uid ignored - * @param array $accessList who has access to the file contains the key 'users' and 'public' - * @return bool - */ public function update(string $path, string $uid, array $accessList): bool { if (empty($accessList)) { if (isset(self::$rememberVersion[$path])) { @@ -357,12 +306,6 @@ public function update(string $path, string $uid, array $accessList): bool { return true; } - /** - * should the file be encrypted or not - * - * @param string $path - * @return bool - */ public function shouldEncrypt(string $path): bool { if ($this->util->shouldEncryptHomeStorage() === false) { $storage = $this->util->getStorage($path); @@ -389,23 +332,18 @@ public function shouldEncrypt(string $path): bool { } /** - * get size of the unencrypted payload per block. - * Nextcloud read/write files with a block size of 8192 byte + * Get size of the unencrypted payload per block. + * Nextcloud reads/writes files with a block size of 8192 byte. * - * Encrypted blocks have a 22-byte IV and 2 bytes of padding, encrypted and + * 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 + * 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 + * 8168 * (3/4) = 6126 bytes (in each base64-encoded unsigned unencrypted block) + * 8096 * (3/4) = 6072 bytes (in each base64-encoded signed unencrypted block) */ public function getUnencryptedBlockSize(bool $signed = false): int { if ($this->useLegacyBase64Encoding) { @@ -415,15 +353,6 @@ public function getUnencryptedBlockSize(bool $signed = false): int { } } - /** - * check if the encryption module is able to read the file, - * e.g. if all encryption keys exists - * - * @param string $path - * @param string $uid user for whom we want to check if they can read the file - * @return bool - * @throws DecryptionFailedException - */ public function isReadable(string $path, string $uid): bool { $fileKey = $this->keyManager->getFileKey($path, null); if (empty($fileKey)) { @@ -445,38 +374,14 @@ public function isReadable(string $path, string $uid): bool { return true; } - /** - * Initial encryption of all files - * - * @param InputInterface $input - * @param OutputInterface $output write some status information to the terminal during encryption - */ public function encryptAll(InputInterface $input, OutputInterface $output): void { $this->encryptAll->encryptAll($input, $output); } - /** - * prepare module to perform decrypt all operation - * - * @param InputInterface $input - * @param OutputInterface $output - * @param string $user - * @return bool - */ public function prepareDecryptAll(InputInterface $input, OutputInterface $output, string $user = ''): bool { return $this->decryptAll->prepare($input, $output, $user); } - /** - * Check if the module is ready to be used by that specific user. - * In case a module is not ready - because e.g. key pairs have not been generated - * upon login this method can return false before any operation starts and might - * cause issues during operations. - * - * @param string $user - * @return bool - * @since 9.1.0 - */ public function isReadyForUser(string $user): bool { if ($this->util->isMasterKeyEnabled()) { return true; @@ -484,14 +389,6 @@ public function isReadyForUser(string $user): bool { return $this->keyManager->userHasKeys($user); } - /** - * Does the encryption module needs a detailed list of users with access to the file? - * For example if the encryption module uses per-user encryption keys and needs to know - * the users with access to the file to encrypt/decrypt it. - * - * @since 13.0.0 - * @return bool - */ public function needDetailedAccessList(): bool { return !$this->util->isMasterKeyEnabled(); } From ec97a0ddf3c8e745e285a506051a33871378c3d5 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 11:36:56 -0500 Subject: [PATCH 07/24] chore: add docblocks to non-interface helper/utility functions Signed-off-by: Josh --- apps/encryption/lib/Crypto/Encryption.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index 3f53ae32becb5..55b45c0a5990a 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -394,13 +394,16 @@ public function needDetailedAccessList(): bool { } /** - * @param string $path - * @return string + * Converts a versions file path to its canonical user file path. + * + * @param string $path File path (may be a versions path) + * @return string Canonical file path */ protected function getPathToRealFile(string $path): string { $realPath = $path; $parts = explode('/', $path); if ($parts[2] === 'files_versions') { + // e.g., "/user/files_versions/document.txt.v1234567890" --> "/user/files/document.txt" $realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3)); $length = strrpos($realPath, '.'); $realPath = substr($realPath, 0, $length); @@ -410,11 +413,11 @@ protected function getPathToRealFile(string $path): string { } /** - * remove .part file extension and the ocTransferId from the file to get the - * original file name + * Removes the .part extension and ocTransferId from a part file path, + * returning the original file name. * - * @param string $path - * @return string + * @param string $path File path, possibly with .part extension and ocTransferId + * @return string Original file path without temporary upload markers */ protected function stripPartFileExtension(string $path): string { if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { @@ -426,10 +429,10 @@ protected function stripPartFileExtension(string $path): string { } /** - * get owner of a file + * Returns and caches the storage owner for a given file path. * - * @param string $path - * @return string + * @param string $path File path + * @return string User id of file owner */ protected function getOwner(string $path): string { if (!isset($this->owner[$path])) { From fce802f44b1bcae3eb77674a1cadf6ad53ea190a Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 13:14:38 -0500 Subject: [PATCH 08/24] chore: document implementation specific throws Signed-off-by: Josh --- apps/encryption/lib/Crypto/Encryption.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index 55b45c0a5990a..a91f8d824ae70 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -145,6 +145,10 @@ public function begin(string $path, ?string $user, string $mode, array $header, return $result; } + /** + * @throws PublicKeyMissingException + * @throws MultiKeyEncryptException + */ public function end(string $path, string $position = '0'): string { $result = ''; if ($this->isWriteOperation) { From 000b6659d97e8166dd7210cedcfe08654b41b45d Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 13:15:02 -0500 Subject: [PATCH 09/24] chore: drop implementation specific throws from interface Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 836d08349b958..cc95812eae536 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -9,6 +9,7 @@ */ namespace OCP\Encryption; +use OC\Encryption\Exceptions\DecryptionFailedException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -65,9 +66,7 @@ public function begin(string $path, ?string $user, string $mode, array $header, * @return string remained data which should be written to the file in case * of a write operation * - * @throws PublicKeyMissingException * @throws \Exception - * @throws MultiKeyEncryptException * * @since 8.1.0 * @since 9.0.0 parameter $position added From 7fcf4f80ef6420cb5098bb05405ae4e3eb6efef0 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 13:16:51 -0500 Subject: [PATCH 10/24] chore: Update psalm-baseline.xml Signed-off-by: Josh --- build/psalm-baseline.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 155ccd6ba42f4..0e182e41bb087 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -1083,9 +1083,6 @@ - - - From 0ecc19257ce7d544f6edda582368cb918f7133cc Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 19:00:21 -0500 Subject: [PATCH 11/24] chore: fixup to make $uid nullable for now Signed-off-by: Josh --- apps/encryption/lib/Crypto/Encryption.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index a91f8d824ae70..5f2fe8dee4736 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -266,7 +266,7 @@ public function decrypt(string $data, string $position = '0'): string { ); } - public function update(string $path, string $uid, array $accessList): bool { + public function update(string $path, ?string $uid, array $accessList): bool { if (empty($accessList)) { if (isset(self::$rememberVersion[$path])) { $this->keyManager->setVersion($path, self::$rememberVersion[$path], new View()); @@ -357,7 +357,7 @@ public function getUnencryptedBlockSize(bool $signed = false): int { } } - public function isReadable(string $path, string $uid): bool { + public function isReadable(string $path, ?string $uid): bool { $fileKey = $this->keyManager->getFileKey($path, null); if (empty($fileKey)) { $owner = $this->util->getOwner($path); From 5aaddf10487c76b654b0eb6061feeca562bd1f36 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 14 Jan 2026 19:01:55 -0500 Subject: [PATCH 12/24] chore: permit nullable $uid in IEncryptionModule.php... Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index cc95812eae536..5b5dc1382ccfc 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -105,12 +105,12 @@ public function decrypt(string $data, string $position = '0'): string; * update encrypted file, e.g. give additional users access to the file * * @param string $path path to the file which should be updated - * @param string $uid of the user who performs the operation + * @param null|string $uid of the user who performs the operation * @param array $accessList who has access to the file contains the key 'users' and 'public' * @return bool * @since 8.1.0 */ - public function update(string $path, string $uid, array $accessList): bool; + public function update(string $path, ?string $uid, array $accessList): bool; /** * should the file be encrypted or not @@ -136,11 +136,11 @@ public function getUnencryptedBlockSize(bool $signed = false): int; * e.g. if all encryption keys exists * * @param string $path - * @param string $uid user for whom we want to check if they can read the file + * @param null|string $uid user for whom we want to check if they can read the file * @return bool * @since 8.1.0 */ - public function isReadable(string $path, string $uid): bool; + public function isReadable(string $path, ?string $uid): bool; /** * Initial encryption of all files From fddd5f469d38bb08e37640f93f60dcdcab575068 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 09:20:57 -0500 Subject: [PATCH 13/24] refactor(encryption): Distinguish blockId from position One is always an int but the other is a string and often more than than the raw position, but an id marker (e.g. `5end`). Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 5b5dc1382ccfc..2692fa16a9567 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -61,7 +61,7 @@ public function begin(string $path, ?string $user, string $mode, array $header, * buffer. * * @param string $path to the file - * @param string $position id of the last block (looks like "end") + * @param string $blockId Block identifier of the last block (looks like "end") * * @return string remained data which should be written to the file in case * of a write operation @@ -71,26 +71,28 @@ public function begin(string $path, ?string $user, string $mode, array $header, * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function end(string $path, string $position = '0'): string; + public function end(string $path, string $blockId = '0'): string; /** * encrypt data * * @param string $data you want to encrypt - * @param string $position position of the block we want to encrypt (starts with '0') + * @param string $blockId Block identifier representing the block index we want to encrypt + * (starts with '0'). Usually a numeric string (e.g. "0", "5"), but + * may have a special marker, such as "5end" to denote the final block. * * @return string encrypted data * * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function encrypt(string $data, string $position = '0'): string; + public function encrypt(string $data, string $blockId = '0'): string; /** * decrypt data * * @param string $data you want to decrypt - * @param string $position position of the block we want to decrypt + * @param string $blockId Block identifier representing the block index we want to decrypt * * @return string decrypted data * @@ -99,13 +101,13 @@ public function encrypt(string $data, string $position = '0'): string; * @since 8.1.0 * @since 9.0.0 parameter $position added */ - public function decrypt(string $data, string $position = '0'): string; + public function decrypt(string $data, string $blockId = '0'): string; /** * update encrypted file, e.g. give additional users access to the file * * @param string $path path to the file which should be updated - * @param null|string $uid of the user who performs the operation + * @param null|string $uid of the user who performs the operation (null for public access). * @param array $accessList who has access to the file contains the key 'users' and 'public' * @return bool * @since 8.1.0 From 77fc7b3237d5629be1ae7ad885cffa5a5ff422bc Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 09:25:21 -0500 Subject: [PATCH 14/24] chore: swap position for blockId in Encryption module implementation Signed-off-by: Josh --- apps/encryption/lib/Crypto/Encryption.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/encryption/lib/Crypto/Encryption.php b/apps/encryption/lib/Crypto/Encryption.php index 5f2fe8dee4736..e3f4f5a155279 100644 --- a/apps/encryption/lib/Crypto/Encryption.php +++ b/apps/encryption/lib/Crypto/Encryption.php @@ -149,7 +149,7 @@ public function begin(string $path, ?string $user, string $mode, array $header, * @throws PublicKeyMissingException * @throws MultiKeyEncryptException */ - public function end(string $path, string $position = '0'): string { + public function end(string $path, string $blockId = '0'): string { $result = ''; if ($this->isWriteOperation) { // in case of a part file we remember the new signature versions @@ -160,7 +160,7 @@ public function end(string $path, string $position = '0'): string { self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1; } if (!empty($this->writeCache)) { - $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position); + $result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $blockId); $this->writeCache = ''; } $publicKeys = []; @@ -198,7 +198,7 @@ public function end(string $path, string $position = '0'): string { return $result ?: ''; } - public function encrypt(string $data, string $position = '0'): string { + public function encrypt(string $data, string $blockId = '0'): string { // If extra data is left over from the last round, make sure it // is integrated into the next block if ($this->writeCache) { @@ -235,7 +235,12 @@ public function encrypt(string $data, string $position = '0'): string { // Read the chunk from the start of $data $chunk = substr($data, 0, $this->getUnencryptedBlockSize(true)); - $encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position); + $encrypted .= $this->crypt->symmetricEncryptFileContent( + $chunk, + $this->fileKey, + $this->version + 1, + $blockId + ); // Remove the chunk we just processed from // $data, leaving only unprocessed data in $data @@ -247,7 +252,7 @@ public function encrypt(string $data, string $position = '0'): string { return $encrypted; } - public function decrypt(string $data, string $position = '0'): string { + public function decrypt(string $data, string $blockId = '0'): string { if (empty($this->fileKey)) { $msg = 'Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'; $hint = $this->l->t('Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'); @@ -261,7 +266,7 @@ public function decrypt(string $data, string $position = '0'): string { $this->fileKey, $this->cipher, $this->version, - $position, + $blockId, !$this->useLegacyBase64Encoding ); } From f8b48130f2f188aa45475bad1bf2bea06905eb52 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 09:41:58 -0500 Subject: [PATCH 15/24] refactor(encryption): differentiate blockId from $this->position Signed-off-by: Josh --- lib/private/Files/Stream/Encryption.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index ef147ec421fb1..52224f13637e5 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -392,8 +392,9 @@ public function stream_seek($offset, $whence = SEEK_SET) { public function stream_close() { $this->flush('end'); - $position = (int)floor($this->position / $this->unencryptedBlockSize); - $remainingData = $this->encryptionModule->end($this->fullPath, $position . 'end'); + $blockIndex = (int)floor($this->position / $this->unencryptedBlockSize); + $blockId = (string)$blockIndex . 'end'; + $remainingData = $this->encryptionModule->end($this->fullPath, $blockId); if ($this->readOnly === false) { if (!empty($remainingData)) { parent::stream_write($remainingData); @@ -407,7 +408,10 @@ public function stream_close() { $cacheEntry = $cache->get($this->internalPath); if ($cacheEntry) { $version = $cacheEntry['encryptedVersion'] + 1; - $cache->update($cacheEntry->getId(), ['encrypted' => $version, 'encryptedVersion' => $version, 'unencrypted_size' => $this->unencryptedSize]); + $cache->update( + $cacheEntry->getId(), + ['encrypted' => $version, 'encryptedVersion' => $version, 'unencrypted_size' => $this->unencryptedSize] + ); } } @@ -425,8 +429,9 @@ protected function flush($positionPrefix = '') { // 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 - $position = (int)floor($this->position / $this->unencryptedBlockSize); - $encrypted = $this->encryptionModule->encrypt($this->cache, $position . $positionPrefix); + $blockIndex = (int)floor($this->position / $this->unencryptedBlockSize); + $blockId = (string)$blockIndex . $positionPrefix); + $encrypted = $this->encryptionModule->encrypt($this->cache, $blockId); $bytesWritten = parent::stream_write($encrypted); $this->writeFlag = false; // Check whether the write concerns the last block @@ -453,12 +458,14 @@ protected function readCache() { if ($this->cache === '' && !($this->position === $this->unencryptedSize && ($this->position % $this->unencryptedBlockSize) === 0)) { // Get the data from the file handle $data = $this->stream_read_block($this->util->getBlockSize()); - $position = (int)floor($this->position / $this->unencryptedBlockSize); + $blockIndex = (int)floor($this->position / $this->unencryptedBlockSize); $numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize); - if ($numberOfChunks === $position) { - $position .= 'end'; + if ($numberOfChunks === $blockIndex) { + $blockId = $blockIndex . 'end'; + } else { + $blockId = $blockIndex; } - $this->cache = $this->encryptionModule->decrypt($data, $position); + $this->cache = $this->encryptionModule->decrypt($data, $blockId); } } From f204f758931976a35d8ad75a64123d799f0688b9 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 10:39:55 -0500 Subject: [PATCH 16/24] test(encryption): fix for typing + make testUser1/testUser2 realistic Signed-off-by: Josh --- .../tests/Crypto/EncryptionTest.php | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/apps/encryption/tests/Crypto/EncryptionTest.php b/apps/encryption/tests/Crypto/EncryptionTest.php index df9655cd2eb08..70466ea0fbf2c 100644 --- a/apps/encryption/tests/Crypto/EncryptionTest.php +++ b/apps/encryption/tests/Crypto/EncryptionTest.php @@ -95,8 +95,44 @@ public function testEndUser1(): void { ->method('decryptAllModeActivated') ->willReturn(false); + // Mocks for methods needed before begin() + $this->keyManagerMock->expects($this->any()) + ->method('getFileKey') + ->willReturn('fileKey'); + $this->cryptMock->expects($this->any()) + ->method('getCipher') + ->willReturn('AES-256-CTR'); + $this->cryptMock->expects($this->any()) + ->method('getLegacyCipher') + ->willReturn('AES-128-CFB'); + $this->cryptMock->expects($this->any()) + ->method('useLegacyBase64Encoding') + ->willReturn(false); + $this->cryptMock->expects($this->any()) + ->method('generateFileKey') + ->willReturn('fileKey'); + + // Prepare the post-begin mocks for end() phase + $this->keyManagerMock->expects($this->any()) + ->method('getPublicKey') + ->willReturnCallback([$this, 'getPublicKeyCallback']); + $this->keyManagerMock->expects($this->any()) + ->method('addSystemKeys') + ->willReturnCallback([$this, 'addSystemKeysCallback']); + $this->cryptMock->expects($this->any()) + ->method('multiKeyEncrypt') + ->willReturn([ + 'user1' => 'encForUser1', + 'user3' => 'encForUser3', + ]); + + // Begin the encryption process as user1, with user2 missing their public key $this->instance->begin('/foo/bar', 'user1', 'r', [], ['users' => ['user1', 'user2', 'user3']]); - $this->endTest(); + + // Set internal state to simulate write and call end() + self::invokePrivate($this->instance, 'isWriteOperation', [true]); + self::invokePrivate($this->instance, 'writeCache', ['']); + $this->instance->end('/foo/bar'); } /** @@ -108,36 +144,43 @@ public function testEndUser2(): void { ->method('decryptAllModeActivated') ->willReturn(false); - $this->expectException(PublicKeyMissingException::class); - - $this->instance->begin('/foo/bar', 'user2', 'r', [], ['users' => ['user1', 'user2', 'user3']]); - $this->endTest(); - } - - /** - * common part of testEndUser1 and testEndUser2 - * - * @throws PublicKeyMissingException - */ - public function endTest() { - // prepare internal variables - self::invokePrivate($this->instance, 'isWriteOperation', [true]); - self::invokePrivate($this->instance, 'writeCache', ['']); + // Mocks for methods needed before begin() + $this->keyManagerMock->expects($this->any()) + ->method('getFileKey') + ->willReturn('fileKey'); + $this->cryptMock->expects($this->any()) + ->method('getCipher') + ->willReturn('AES-256-CTR'); + $this->cryptMock->expects($this->any()) + ->method('getLegacyCipher') + ->willReturn('AES-128-CFB'); + $this->cryptMock->expects($this->any()) + ->method('useLegacyBase64Encoding') + ->willReturn(false); + $this->cryptMock->expects($this->any()) + ->method('generateFileKey') + ->willReturn('fileKey'); + // Prepare the post-begin mocks for end() phase $this->keyManagerMock->expects($this->any()) ->method('getPublicKey') ->willReturnCallback([$this, 'getPublicKeyCallback']); $this->keyManagerMock->expects($this->any()) ->method('addSystemKeys') ->willReturnCallback([$this, 'addSystemKeysCallback']); - $this->cryptMock->expects($this->any()) - ->method('multiKeyEncrypt') - ->willReturn([]); + $this->cryptMock->expects($this->never()) + ->method('multiKeyEncrypt'); + + $this->expectException(PublicKeyMissingException::class); + + $this->instance->begin('/foo/bar', 'user2', 'r', [], ['users' => ['user1', 'user2', 'user3']]); + // Set internal state to simulate write and call end() + self::invokePrivate($this->instance, 'isWriteOperation', [true]); + self::invokePrivate($this->instance, 'writeCache', ['']); $this->instance->end('/foo/bar'); } - public function getPublicKeyCallback($uid) { if ($uid === 'user2') { throw new PublicKeyMissingException($uid); From ccfc7ad81e40c02b53b0218c70172d512957fcab Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 15:28:16 -0500 Subject: [PATCH 17/24] chore: fixup typo Signed-off-by: Josh --- lib/private/Files/Stream/Encryption.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 52224f13637e5..365ce530e1426 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -430,7 +430,7 @@ protected function flush($positionPrefix = '') { // we are handling that separately here and we don't want to // get into an infinite loop $blockIndex = (int)floor($this->position / $this->unencryptedBlockSize); - $blockId = (string)$blockIndex . $positionPrefix); + $blockId = (string)$blockIndex . $positionPrefix; $encrypted = $this->encryptionModule->encrypt($this->cache, $blockId); $bytesWritten = parent::stream_write($encrypted); $this->writeFlag = false; From bfaf55c4a6c3240ca0e15a4982a3455abd678aab Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 16:13:42 -0500 Subject: [PATCH 18/24] chore: add proper cast Signed-off-by: Josh --- lib/private/Files/Stream/Encryption.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 365ce530e1426..83c0da59cd8ca 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -461,9 +461,9 @@ protected function readCache() { $blockIndex = (int)floor($this->position / $this->unencryptedBlockSize); $numberOfChunks = (int)($this->unencryptedSize / $this->unencryptedBlockSize); if ($numberOfChunks === $blockIndex) { - $blockId = $blockIndex . 'end'; + $blockId = (string)$blockIndex . 'end'; } else { - $blockId = $blockIndex; + $blockId = (string)$blockIndex; } $this->cache = $this->encryptionModule->decrypt($data, $blockId); } From fff8fc71edd071f57f91dbf080e301981fd8dd96 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 21:43:13 -0500 Subject: [PATCH 19/24] chore: add formerly misssing getOwner from EncryptionTest Signed-off-by: Josh --- apps/encryption/tests/Crypto/EncryptionTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/encryption/tests/Crypto/EncryptionTest.php b/apps/encryption/tests/Crypto/EncryptionTest.php index 70466ea0fbf2c..8020e3b5dcb19 100644 --- a/apps/encryption/tests/Crypto/EncryptionTest.php +++ b/apps/encryption/tests/Crypto/EncryptionTest.php @@ -325,6 +325,10 @@ public function testUpdate($fileKey, $expected): void { $this->keyManagerMock->expects($this->never())->method('getVersion'); $this->keyManagerMock->expects($this->never())->method('setVersion'); + $this->utilMock->expects($this->any()) + ->method('getOwner') + ->willReturn('user1'); + $this->assertSame($expected, $this->instance->update('path', 'user1', ['users' => ['user1']]) ); @@ -385,6 +389,10 @@ function ($fileKey, $publicKeys) { $this->keyManagerMock->expects($this->never())->method('getVersion'); $this->keyManagerMock->expects($this->never())->method('setVersion'); + $this->utilMock->expects($this->any()) + ->method('getOwner') + ->willReturn('user1'); + $this->assertTrue( $this->instance->update('path', 'user1', ['users' => ['user1']]) ); From df6b518308d0d0708ee4dfdaf20098b7bdf9b6de Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 22:18:18 -0500 Subject: [PATCH 20/24] test: add missing stubs to EncryptionTest Signed-off-by: Josh --- apps/encryption/tests/Crypto/EncryptionTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/encryption/tests/Crypto/EncryptionTest.php b/apps/encryption/tests/Crypto/EncryptionTest.php index 8020e3b5dcb19..e974fd9696a73 100644 --- a/apps/encryption/tests/Crypto/EncryptionTest.php +++ b/apps/encryption/tests/Crypto/EncryptionTest.php @@ -126,6 +126,10 @@ public function testEndUser1(): void { 'user3' => 'encForUser3', ]); + $this->utilMock->expects($this->any()) + ->method('getOwner') + ->willReturn('user1'); + // Begin the encryption process as user1, with user2 missing their public key $this->instance->begin('/foo/bar', 'user1', 'r', [], ['users' => ['user1', 'user2', 'user3']]); @@ -278,6 +282,13 @@ public function testBeginDecryptAll(): void { ->with($path, null, true) ->willReturn($fileKey); + $this->cryptMock->expects($this->any()) + ->method('getCipher') + ->willReturn('AES-256-CTR'); + $this->cryptMock->expects($this->any()) + ->method('getLegacyCipher') + ->willReturn('AES-128-CFB'); + $this->instance->begin($path, 'user', 'r', [], []); $this->assertSame($fileKey, From a35de2bcf8558f81c4216d9cc33cd5339f1e95bc Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 22:32:52 -0500 Subject: [PATCH 21/24] test(encryption): add stub for needDetailedAccessList to UpdateTest Signed-off-by: Josh --- tests/lib/Encryption/UpdateTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lib/Encryption/UpdateTest.php b/tests/lib/Encryption/UpdateTest.php index 04ca224c0a132..d00586e4f4c16 100644 --- a/tests/lib/Encryption/UpdateTest.php +++ b/tests/lib/Encryption/UpdateTest.php @@ -92,6 +92,7 @@ public function testUpdate($path, $isDir, $allFiles, $numberOfFiles): void { $this->encryptionManager->expects($this->once()) ->method('getEncryptionModule') ->willReturn($this->encryptionModule); + $this->encryptionModule->method('needDetailedAccessList')->willReturn(true); if ($isDir) { $this->util->expects($this->once()) From 67f8e6dd35b4dfc1259495ed0796b26989866add Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 15 Jan 2026 23:13:04 -0500 Subject: [PATCH 22/24] chore: add stub for getLegacyCipher to EncryptionTest Signed-off-by: Josh --- apps/encryption/tests/Crypto/EncryptionTest.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/encryption/tests/Crypto/EncryptionTest.php b/apps/encryption/tests/Crypto/EncryptionTest.php index e974fd9696a73..f7fcc49ec120e 100644 --- a/apps/encryption/tests/Crypto/EncryptionTest.php +++ b/apps/encryption/tests/Crypto/EncryptionTest.php @@ -306,10 +306,18 @@ public function testBeginInitMasterKey(): void { ->method('decryptAllModeActivated') ->willReturn(false); - $this->sessionMock->expects($this->once())->method('isReady')->willReturn(false); - $this->utilMock->expects($this->once())->method('isMasterKeyEnabled') + $this->sessionMock->expects($this->once()) + ->method('isReady') + ->willReturn(false); + $this->utilMock->expects($this->once()) + ->method('isMasterKeyEnabled') ->willReturn(true); - $this->keyManagerMock->expects($this->once())->method('init')->with('', ''); + $this->keyManagerMock->expects($this->once()) + ->method('init') + ->with('', ''); + $this->cryptMock->expects($this->any()) + ->method('getLegacyCipher') + ->willReturn('anyWillDo'); $this->instance->begin('/user/files/welcome.txt', 'user', 'r', [], []); } From d4a1f47fa27205078238adbfc2330475fd9b7e65 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 17 Jan 2026 20:23:19 -0500 Subject: [PATCH 23/24] chore: fixup for lint Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 2692fa16a9567..4b3050ff36b16 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -78,8 +78,8 @@ public function end(string $path, string $blockId = '0'): string; * * @param string $data you want to encrypt * @param string $blockId Block identifier representing the block index we want to encrypt - * (starts with '0'). Usually a numeric string (e.g. "0", "5"), but - * may have a special marker, such as "5end" to denote the final block. + * (starts with '0'). Usually a numeric string (e.g. "0", "5"), but + * may have a special marker, such as "5end" to denote the final block. * * @return string encrypted data * From 5947bef26727943097d8ae921ddb1e2b21849723 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 17 Jan 2026 20:26:59 -0500 Subject: [PATCH 24/24] chore: lint Signed-off-by: Josh --- lib/public/Encryption/IEncryptionModule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/Encryption/IEncryptionModule.php b/lib/public/Encryption/IEncryptionModule.php index 4b3050ff36b16..49424a0949f45 100644 --- a/lib/public/Encryption/IEncryptionModule.php +++ b/lib/public/Encryption/IEncryptionModule.php @@ -78,8 +78,8 @@ public function end(string $path, string $blockId = '0'): string; * * @param string $data you want to encrypt * @param string $blockId Block identifier representing the block index we want to encrypt - * (starts with '0'). Usually a numeric string (e.g. "0", "5"), but - * may have a special marker, such as "5end" to denote the final block. + * (starts with '0'). Usually a numeric string (e.g. "0", "5"), but + * may have a special marker, such as "5end" to denote the final block. * * @return string encrypted data *