Compare commits

...

15 Commits

Author SHA1 Message Date
Josh
e127253245 chore: add note re: legacy file key detection logic
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-07 11:17:20 -04:00
Josh
6a010dcfef chore: add follow-up item in results re: useLegacyFileKey value
Add comment to clarify hardcoded useLegacyFileKey value.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-07 11:04:25 -04:00
Josh
e17e610987 refactor(encryption): update begin method
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-07 10:27:15 -04:00
Josh
f1f1b14e1f chore: more refactoring
Refactor encryption methods for improved clarity and robustness, including better error handling and parameter descriptions.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 23:46:01 -04:00
Josh
9447c564a5 chore: Clarify comment for addSystemKeys method
Updated the comment to clarify the public link share key.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 22:55:29 -04:00
Josh
01eba7a2a1 chore: lint
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 19:24:25 -04:00
Josh
a8b7a01ef3 refactor(encryption): revise update() method
Clarity and robustiness

Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 19:18:35 -04:00
Josh
d0b9a608ad refactor(Encryption): add type hints + docs [first pass]
Updated the Encryption class to use typed properties and added PHPDoc comments for better clarity and type hinting.

Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 18:56:01 -04:00
Josh
aab70ac84b chore: Furthr improve docs in IEncryptionModule interface
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 12:40:07 -04:00
Josh
d4e2146ab5 chore: Add/fix type hints and improve docs in IEncryptionModule
Signed-off-by: Josh <josh.t.richards@gmail.com>
2025-10-06 12:22:27 -04:00
Ferdinand Thiessen
cb1a6f79e7 Merge pull request #55442 from nextcloud/refactor/migrate-cypress-tests
test: migrate `LoginForm` component test to vitest
2025-10-06 17:41:46 +02:00
Ferdinand Thiessen
2fd87955e6 Merge pull request #55525 from nextcloud/chore/karma-commets
refactor(core): migrate tests of `OCP.Comments` to `vitest`
2025-10-06 17:10:29 +02:00
Ferdinand Thiessen
62539ecacf chore: compile assets
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-10-06 15:18:51 +02:00
Ferdinand Thiessen
ba01412389 test: migrate LoginForm component test to vitest
Cypress has some limitations:
- its vue 2 supported was removed
- it fails with our vue 3 migration due to 2 different vue versions
  being present
- its slow compared to vitest

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-10-06 15:18:51 +02:00
Ferdinand Thiessen
769ec69bb3 refactor(core): migrate tests of OCP.Comments to vitest
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2025-10-06 15:05:31 +02:00
13 changed files with 692 additions and 510 deletions

View File

@@ -8,6 +8,7 @@
namespace OCA\Encryption\Crypto;
use OC\Encryption\Exceptions\DecryptionFailedException;
use OC\Encryption\Exceptions\EncryptionFailedException;
use OC\Files\Cache\Scanner;
use OC\Files\View;
use OCA\Encryption\Exceptions\MultiKeyEncryptException;
@@ -21,42 +22,108 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Implements an encryption module for Nextcloud's Server-side Encryption (SSE).
*
* @since 9.1.0
*/
class Encryption implements IEncryptionModule {
// The technical ID for this encryption module. Must be unique.
public const ID = 'OC_DEFAULT_MODULE';
// Human-readable name of this encryption module.
public const DISPLAY_NAME = 'Default encryption module';
/** @var string */
private $cipher;
/**
* The cipher algorithm used for encrypting and decrypting file contents (e.g., AES-256-CFB).
* Set during file operations based on file header or defaults.
*
* @var string
*/
private string $cipher;
/** @var string */
private $path;
/**
* The absolute path to the file being processed.
* Used for key management and file operations.
*
* @var string
*/
private string $path;
/** @var string */
private $user;
/**
* Username of the user performing the read/write operation.
*
* @var string
*/
private string $user;
/**
* Cached map of file paths to their respective owners.
* Used to avoid repeated lookups.
*
* @var array<string, string>
*/
private array $owner;
/** @var string */
private $fileKey;
/**
* The encryption key used for the current file operation.
*
* @var string
*/
private string $fileKey;
/** @var string */
private $writeCache;
/**
* Buffer/cache for data that has not yet been encrypted and written.
* Used for block-wise encryption.
*
* @var string
*/
private string $writeCache;
/** @var array */
private $accessList;
/**
* List of users and public entities that have access to the file.
* Contains the keys 'users' and 'public'.
*
* @var array
*/
private array $accessList;
/** @var boolean */
private $isWriteOperation;
/**
* Indicates whether the current operation is a write operation.
*
* @var bool
*/
private bool $isWriteOperation;
/**
* Indicates whether the master password (master key) is being used for encryption.
*
* @var bool
*/
private bool $useMasterPassword;
/**
* Flag for whether legacy base64 encoding is used for file encryption.
* Legacy encoding affects block size calculation and compatibility.
*
* @var bool
*/
private bool $useLegacyBase64Encoding = false;
/** @var int Current version of the file */
/**
* The current version of the file being processed.
* Used for key and signature versioning.
*
* @var int
*/
private int $version = 0;
/** @var array remember encryption signature version */
private static $rememberVersion = [];
/**
* Static cache mapping file paths to remembered encryption signature versions.
* Used during multipart and update operations.
*
* @var array<string, int>
*/
private static array $rememberVersion = [];
public function __construct(
private Crypt $crypt,
@@ -73,103 +140,118 @@ class Encryption implements IEncryptionModule {
}
/**
* @return string defining the technical unique id
* Returns the technical unique ID of the encryption module.
*
* @return string Technical unique ID.
*/
public function getId() {
public function getId(): string {
return self::ID;
}
/**
* In comparison to getKey() this function returns a human readable (maybe translated) name
* Unlike getId(), this function returns a human-readable (possibly translated) name of the encryption module.
*
* @return string
* @return string Display name of the encryption module.
*/
public function getDisplayName() {
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
* Initializes the encryption or decryption process for a file.
*
* @param string $path to the file
* @param string $user who read/write the file
* @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'
* i.e. Start receiving chunks from a file. This is the place to perform any initial steps
* before starting encryption/decryption of the chunks.
*
* @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
* @param string $path Path to the file being processed.
* @param string $user User performing the operation.
* @param string $mode Operation mode ('r' for read, 'w' for write).
* @param array $header File encryption metadata/header.
* @param array $accessList List of users/keys with access.
* @return array Metadata for further processing (i.e. header data key-value pairs if writing or empty array if done).
*/
public function begin($path, $user, $mode, array $header, array $accessList) {
public function begin(string $path, string $user, string $mode, array $header, array $accessList): array {
// All write-capable modes supported by Nextcloud.
$writeModes = ['w', 'w+', 'wb', 'wb+'];
// TODO: Also sanitize non-write modes rather than just assuming read mode
// Initialize properties.
$this->path = $this->getPathToRealFile($path);
$this->accessList = $accessList;
$this->user = $user;
$this->isWriteOperation = false;
$this->accessList = $accessList;
$this->writeCache = '';
$this->useLegacyBase64Encoding = true;
// Determine if this is a write operation
$this->isWriteOperation = in_array($mode, $writeModes, true);
if (isset($header['encoding'])) {
$this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT;
// Ensure encryption session is ready; if using master key initialize it
if (!$this->session->isReady() && $this->util->isMasterKeyEnabled()) {
$this->keyManager->init('', ''); // empty password and username
}
if ($this->session->isReady() === false) {
// if the master key is enabled we can initialize encryption
// with a empty password and user name
if ($this->util->isMasterKeyEnabled()) {
$this->keyManager->init('', '');
}
// Detect legacy file key usage.
$useLegacyFileKey = null;
if (isset($header['useLegacyFileKey'])) {
// NOTE: null means "try both" {@see OCA\Encryption\KeyManager::getFileKey()}
// XXX: If 'true' we could probably explicitly treat it as bool true rather than null/both?
$useLegacyFileKey = ($header['useLegacyFileKey'] === 'false') ? false : null;
}
/* If useLegacyFileKey is not specified in header, auto-detect, to be safe */
$useLegacyFileKey = (($header['useLegacyFileKey'] ?? '') == 'false' ? false : null);
// Always use the version from the original file.
// Also part files need to have a correct version number if they get moved to the final location.
$strippedPath = $this->util->stripPartialFileExtension($path);
$this->version = (int)$this->keyManager->getVersion($strippedPath, new View());
$this->fileKey = $this->keyManager->getFileKey($this->path, $useLegacyFileKey, $this->session->decryptAllModeActivated());
// always use the version from the original file, also part files
// need to have a correct version number if they get moved over to the
// final location
$this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
if (
$mode === 'w'
|| $mode === 'w+'
|| $mode === 'wb'
|| $mode === 'wb+'
) {
$this->isWriteOperation = true;
// Determine if "decrypt all" mode is enabled for the current session (influences how file key is retrieved)
$decryptAllMode = $this->session->decryptAllModeActivated();
// Retrieve appropriate file key based on mode/etc for the user
$this->fileKey = $this->keyManager->getFileKey($this->path, $useLegacyFileKey, $decryptAllMode);
// Set cipher and and encoding file key based on operation mode.
if ($this->isWriteOperation) {
// For write operations:
// - Use the cipher configured for writing.
// - Update encoding if required by cipher.
// - Generate key file for the user (if it doesn't exist).
$this->cipher = $this->crypt->getCipher();
$this->useLegacyBase64Encoding = $this->crypt->useLegacyBase64Encoding();
if (empty($this->fileKey)) {
// Generate a new file key if none exists (new file or new encryption context)
$this->fileKey = $this->crypt->generateFileKey();
}
} else {
// if we read a part file we need to increase the version by 1
// because the version number was also increased by writing
// the part file
if (Scanner::isPartialFile($path)) {
$this->version = $this->version + 1;
// For read operations:
// - Use the encoding specified in the header, otherwise assume legacy (<=oC6).
// - Use the cipher specified in the header, otherwise assume legacy (<=oC6).
if (isset($header['cipher'])) {
$this->cipher = $header['cipher'];
} else {
$this->cipher = $this->crypt->getLegacyCipher();
// TODO: Optionally log legacy / heading missing fallback (at debug or info level)
}
if (isset($header['encoding'])) {
$this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT;
} else {
$this->useLegacyBase64Encoding = true;
}
}
if ($this->isWriteOperation) {
$this->cipher = $this->crypt->getCipher();
$this->useLegacyBase64Encoding = $this->crypt->useLegacyBase64Encoding();
} elseif (isset($header['cipher'])) {
$this->cipher = $header['cipher'];
} else {
// if we read a file without a header we fall-back to the legacy cipher
// which was used in <=oC6
$this->cipher = $this->crypt->getLegacyCipher();
// TODO: Decide when we can safely remove some of the legacy (<=oC6) stuff above (and elsewhere)
// If we read a part file we need to increase the version by 1
// because the version number was also increased by writing the part file.
if (!$this->isWriteOperation && Scanner::isPartialFile($path)) {
$this->version = $this->version + 1;
}
// Prepare metadata for the caller (used for header updates, logging, etc).
$result = [
'cipher' => $this->cipher,
'signed' => 'true',
// XXX: Confirm hardcoding this is correct; shouldn't this be set based on value of $useLegacyFileKey?
'useLegacyFileKey' => 'false',
];
if ($this->useLegacyBase64Encoding !== true) {
$result['encoding'] = Crypt::BINARY_ENCODING_FORMAT;
}
@@ -178,223 +260,299 @@ class Encryption implements IEncryptionModule {
}
/**
* 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.
* Finalizes the encryption process for a file, handling buffered data and key updates.
*
* @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
* @param string $path Path to the file being finalized.
* @param string $position Position in the file (for block-wise encryption).
* @return string Remaining encrypted data to be written, if any.
* @throws PublicKeyMissingException
* @throws \Exception
* @throws MultiKeyEncryptException
* @throws \Exception
*/
public function end($path, $position = '0') {
public function end(string $path, string $position = '0'): string {
// Only perform actions if this is a write operation.
if (!$this->isWriteOperation) {
return '';
}
// Remember new signature version for partial files.
if (Scanner::isPartialFile($path)) {
self::$rememberVersion[$this->util->stripPartialFileExtension($path)] = $this->version + 1;
}
// Encrypt any remaining data in the write cache.
$result = '';
if ($this->isWriteOperation) {
// in case of a part file we remember the new signature versions
// the version will be set later on update.
// This way we make sure that other apps listening to the pre-hooks
// still get the old version which should be the correct value for them
if (Scanner::isPartialFile($path)) {
self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
}
if (!empty($this->writeCache)) {
$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
if (!empty($this->writeCache)) {
try {
$result = $this->crypt->symmetricEncryptFileContent(
$this->writeCache,
$this->fileKey,
$this->version + 1,
$position
);
$this->writeCache = '';
} catch (\Throwable $e) {
$this->logger->error('Encryption failure during final block: ' . $e->getMessage(), [
'file' => $this->path ?? $path,
'user' => $this->user ?? 'unknown',
'app' => 'encryption'
]);
throw new EncryptionFailedException(
'Encryption failed during final block.',
$e->getMessage(),
$e->getCode(),
$e
);
}
$publicKeys = [];
if ($this->useMasterPassword === true) {
$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
} else {
foreach ($this->accessList['users'] as $uid) {
try {
$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
} catch (PublicKeyMissingException $e) {
$this->logger->warning(
'no public key found for user "{uid}", user will not be able to read the file',
['app' => 'encryption', 'uid' => $uid]
);
// if the public key of the owner is missing we should fail
if ($uid === $this->user) {
throw $e;
}
}
// Build the list of public keys required for the finalized access list.
$publicKeys = [];
if ($this->useMasterPassword === true) {
$masterKeyId = $this->keyManager->getMasterKeyId();
$publicKeys[$masterKeyId] = $this->keyManager->getPublicMasterKey();
} else {
foreach ($this->accessList['users'] as $accessUid) {
try {
$publicKeys[$accessUid] = $this->keyManager->getPublicKey($accessUid);
} catch (PublicKeyMissingException $e) {
$this->logger->warning(
'No public key found for user "{uid}", user will not be able to read the file.',
['uid' => $accessUid, 'error' => $e->getMessage(), 'app' => 'encryption']
);
// If the public key of the owner is missing we should fail.
if ($accessUid === $this->user) {
throw $e;
}
}
}
$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path));
$shareKeys = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
if (!$this->keyManager->deleteLegacyFileKey($this->path)) {
$this->logger->warning(
'Failed to delete legacy filekey for {path}',
['app' => 'encryption', 'path' => $path]
);
}
foreach ($shareKeys as $uid => $keyFile) {
$this->keyManager->setShareKey($this->path, $uid, $keyFile);
}
}
// Add system-level keys (e.g. public link share keys, recovery keys).
$owner = $this->getOwner($path);
$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $owner);
// Encrypt the file key for all relevant public keys.
$shareKeys = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
// Remove legacy file keys for improved security.
if (!$this->keyManager->deleteLegacyFileKey($this->path)) {
$this->logger->warning(
'Failed to delete legacy filekey for {path}.',
['app' => 'encryption', 'path' => $path]
);
}
// Store the new share keys for each user.
foreach ($shareKeys as $shareKeyUid => $keyFile) {
$this->keyManager->setShareKey($this->path, $shareKeyUid, $keyFile);
}
return $result ?: '';
}
/**
* encrypt data
* Encrypts a chunk of file data.
*
* @param string $data you want to encrypt
* @param int $position
* @return string encrypted data
* @param string $data The plaintext data to encrypt.
* @param int $position The position in the file (for block-wise encryption).
* @return string The encrypted data.
* @throws EncryptionFailedException If encryption fails.
*/
public function encrypt($data, $position = 0) {
// 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
public function encrypt(string $data, int $position = 0): string {
// Integrate leftover buffered data from previous round.
if (!empty($this->writeCache)) {
$data = $this->writeCache . $data;
// Clear the write cache, ready for reuse - it has been
// flushed and its old contents processed
$this->writeCache = '';
}
$encrypted = '';
// While there still remains some data to be processed & written
$blockSize = $this->getUnencryptedBlockSize(true);
// Process data in block-sized chunks.
while (strlen($data) > 0) {
// Remaining length for this iteration, not of the
// entire file (may be greater than 8192 bytes)
$remainingLength = strlen($data);
// If data remaining to be written is less than the
// size of 1 unencrypted block
if ($remainingLength < $this->getUnencryptedBlockSize(true)) {
// Set writeCache to contents of $data
// The writeCache will be carried over to the
// next write round, and added to the start of
// $data to ensure that written blocks are
// always the correct length. If there is still
// data in writeCache after the writing round
// has finished, then the data will be written
// to disk by $this->flush().
// Buffer incomplete blocks for future encryption.
if ($remainingLength < $blockSize) {
$this->writeCache = $data;
// Clear $data ready for next round
$data = '';
} else {
// 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);
// 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->getUnencryptedBlockSize(true));
break;
}
// Encrypt a full block.
$chunk = substr($data, 0, $blockSize);
try {
$encryptedChunk = $this->crypt->symmetricEncryptFileContent(
$chunk,
$this->fileKey,
$this->version + 1,
(string)$position
);
} catch (\Throwable $e) {
$this->logger->error('Encryption failure: ' . $e->getMessage(), [
'file' => $this->path ?? 'unknown',
'user' => $this->user ?? 'unknown',
'app' => 'encryption'
]);
throw new \OC\Encryption\Exceptions\EncryptionFailedException(
'Encryption failed.',
$e->getMessage(),
$e->getCode(),
$e
);
}
$encrypted .= $encryptedChunk;
// Remove processed chunk from data.
$data = substr($data, $blockSize);
}
return $encrypted;
}
/**
* decrypt data
* Decrypts a chunk of encrypted file data.
*
* @param string $data you want to decrypt
* @param int|string $position
* @return string decrypted data
* @throws DecryptionFailedException
* @param string $data The encrypted data to decrypt.
* @param int|string $position Position in the file (for block-wise decryption).
* @return string The decrypted data chunk.
* @throws DecryptionFailedException If the file key is missing or decryption fails.
*/
public function decrypt($data, $position = 0) {
public function decrypt(string $data, int|string $position = 0): string {
// Robustness: Ensure we have the file key before attempting decryption.
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.');
$this->logger->error($msg);
throw new DecryptionFailedException($msg, $hint);
$message = 'Cannot decrypt this file; this is probably a shared file. Please ask the file owner to reshare the file with you.';
$hint = $this->l->t($message);
$this->logger->error($message, [
'file' => $this->path ?? 'unknown',
'user' => $this->user ?? 'unknown',
'app' => 'encryption'
]);
throw new DecryptionFailedException($message, $hint);
}
return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position, !$this->useLegacyBase64Encoding);
// Perform decryption using the cryptographic service.
try {
return $this->crypt->symmetricDecryptFileContent(
$data,
$this->fileKey,
$this->cipher,
$this->version,
(string)$position,
!$this->useLegacyBase64Encoding
);
} catch (\Throwable $e) {
// Robustness: Catch all exceptions, log, and throw a uniform decryption failure.
$errorMsg = 'Decryption failure: ' . $e->getMessage();
$this->logger->error($errorMsg, [
'file' => $this->path ?? 'unknown',
'user' => $this->user ?? 'unknown',
'app' => 'encryption'
]);
throw new DecryptionFailedException('Decryption failed.', $errorMsg);
}
}
/**
* update encrypted file, e.g. give additional users access to the file
* Updates the encrypted files access keys for new access permissions.
*
* @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
* @param string $path Path to the file to update.
* @param string $uid User performing the operation.
* @param array $accessList Access permissions: ['users' => [...], 'public' => [...]].
* @return bool True on success, false otherwise.
*/
public function update($path, $uid, array $accessList) {
public function update(string $path, string $uid, array $accessList): bool {
// If no access list is provided, handle possible remembered version and return early.
if (empty($accessList)) {
if (isset(self::$rememberVersion[$path])) {
$this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
$version = self::$rememberVersion[$path];
$this->keyManager->setVersion($path, $version, new View());
unset(self::$rememberVersion[$path]);
}
return false;
}
// Fetch the file encryption key.
$fileKey = $this->keyManager->getFileKey($path, null);
if (empty($fileKey)) {
$this->logger->debug(
'No file key found; assuming file "{file}" is not encrypted.',
['file' => $path, 'app' => 'encryption']
);
return false;
}
if (!empty($fileKey)) {
$publicKeys = [];
if ($this->useMasterPassword === true) {
$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
} else {
foreach ($accessList['users'] as $user) {
try {
$publicKeys[$user] = $this->keyManager->getPublicKey($user);
} catch (PublicKeyMissingException $e) {
$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
// Build the list of public keys required for the updated access list.
$publicKeys = [];
if ($this->useMasterPassword) {
$masterKeyId = $this->keyManager->getMasterKeyId();
$publicKeys[$masterKeyId] = $this->keyManager->getPublicMasterKey();
} else {
foreach ($accessList['users'] as $accessUid) {
try {
$publicKeys[$accessUid] = $this->keyManager->getPublicKey($accessUid);
} catch (PublicKeyMissingException $e) {
$this->logger->warning(
'No public key found for user "{user}", user will not be able to read the file.',
['user' => $accessUid, 'error' => $e->getMessage(), 'app' => 'encryption']
);
// Robustness: continue so file isn't left inaccessible, but missing keys/users won't have access.
// Exception: If the public key of the owner is missing we should outright fail (or at least pass
// the buck more strongly than just logging the warning like for others).
if ($accessUid === $this->user) {
throw $e;
}
}
}
}
$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
// Add system-level keys (e.g. public link share and recovery keys).
$owner = $this->getOwner($path);
$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $owner);
$shareKeys = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
// Encrypt the file key for all relevant public keys.
$shareKeys = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
$this->keyManager->deleteAllFileKeys($path);
// Remove all previous share keys (security: avoid stale access).
$this->keyManager->deleteAllFileKeys($path);
foreach ($shareKeys as $uid => $keyFile) {
$this->keyManager->setShareKey($path, $uid, $keyFile);
}
} else {
$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
['file' => $path, 'app' => 'encryption']);
return false;
// Write the new share keys for each user.
foreach ($shareKeys as $shareKeyUid => $keyFile) {
$this->keyManager->setShareKey($path, $shareKeyUid, $keyFile);
}
return true;
}
/**
* should the file be encrypted or not
* Determines if a file at the given path should be encrypted based on storage type and path characteristics.
*
* @param string $path
* @return boolean
* @param string $path The file path to check.
* @return bool True if the file should be encrypted, false otherwise.
*/
public function shouldEncrypt($path) {
public function shouldEncrypt(string $path): bool {
// If home storage encryption is disabled, and this is a home storage, don't encrypt
if ($this->util->shouldEncryptHomeStorage() === false) {
$storage = $this->util->getStorage($path);
if ($storage && $storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
return false;
}
}
// Ensure the path has enough segments to be valid for encryption
$parts = explode('/', $path);
if (count($parts) < 4) {
return false;
}
if ($parts[2] === 'files') {
return true;
}
if ($parts[2] === 'files_versions') {
return true;
}
if ($parts[2] === 'files_trashbin') {
// Only encrypt files in certain folders
$encryptFolders = [
'files',
'files_versions',
'files_trashbin'
];
if (in_array($parts[2], $encryptFolders, true)) {
return true;
}
@@ -402,56 +560,67 @@ class Encryption implements IEncryptionModule {
}
/**
* get size of the unencrypted payload per block.
* Nextcloud read/write files with a block size of 8192 byte
* Returns the maximum number of bytes of unencrypted data that can fit in a single file block,
* taking into account encryption overhead and encoding.
*
* Block structure details:
* - Base block size: 8192 bytes
* - Overheads:
* - IV: 22 bytes
* - Padding: 2 bytes (unsigned), +1 byte if signed
* - Signature: 71 bytes (if signed)
* - Legacy base64 encoding further reduces capacity by a factor of 0.75
*
* 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:
* Final sizes:
* - Unsigned binary: 8168 bytes
* - Signed binary: 8096 bytes
* - Unsigned base64: 6126 bytes
* - Signed base64: 6072 bytes
*
* 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
* @param bool $signed Whether the block is cryptographically signed.
* @return int Number of bytes available for unencrypted data.
*/
public function getUnencryptedBlockSize($signed = false) {
if ($this->useLegacyBase64Encoding) {
return $signed ? 6072 : 6126;
} else {
public function getUnencryptedBlockSize(bool $signed = false): int {
if (!$this->useLegacyBase64Encoding) {
return $signed ? 8096 : 8168;
} else {
return $signed ? 6072 : 6126;
}
}
/**
* check if the encryption module is able to read the file,
* e.g. if all encryption keys exists
* Checks if the specified file is readable (decryptable) for the given user.
*
* @param string $path
* @param string $uid user for whom we want to check if they can read the file
* @return bool
* @throws DecryptionFailedException
* @param string $path Path to the file.
* @param string $uid User for whom to check readability.
* @return bool True if readable, false otherwise.
* @throws DecryptionFailedException If the file is shared and the user can't read it.
*/
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);
if ($owner !== $uid) {
// if it is a shared file we throw a exception with a useful
// error message because in this case it means that the file was
// shared with the user at a point where the user didn't had a
// valid private/public key
// If it is a shared file, throw an exception with a useful
// error message, because this means the file was shared
// with the user at a point where the user didn't have a
// valid private/public key.
$msg = 'Encryption module "' . $this->getDisplayName()
. '" is not able to read ' . $path;
$hint = $this->l->t('Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
$msg = sprintf(
'Encryption module "%s" is not able to read %s',
$this->getDisplayName(),
$path
);
$hint = $this->l->t(
'Cannot read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.'
);
$this->logger->warning($msg);
throw new DecryptionFailedException($msg, $hint);
}
return false;
}
@@ -459,67 +628,63 @@ class Encryption implements IEncryptionModule {
}
/**
* Initial encryption of all files
* Initiates encryption of all files using the encryption module.
* This method delegates to the encryptAll service and streams status information to the provided output.
*
* @param InputInterface $input
* @param OutputInterface $output write some status information to the terminal during encryption
* @param InputInterface $input Input interface for user interaction (CLI or otherwise).
* @param OutputInterface $output Output interface for status and progress updates.
*/
public function encryptAll(InputInterface $input, OutputInterface $output) {
public function encryptAll(InputInterface $input, OutputInterface $output): void {
$this->encryptAll->encryptAll($input, $output);
}
/**
* prepare module to perform decrypt all operation
* Prepares the encryption module for the 'decrypt all' operation.
* Delegates to the DecryptAll service, which performs all necessary setup.
*
* @param InputInterface $input
* @param OutputInterface $output
* @param string $user
* @return bool
* @param InputInterface $input The input interface for user interaction.
* @param OutputInterface $output The output interface for user feedback.
* @param string $user (optional) User whose files should be prepared for decryption. If omitted, assumes all users.
* @return bool True if preparation succeeded; false otherwise.
*/
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);
}
/**
* @param string $path
* @return string
*/
protected function getPathToRealFile($path) {
$realPath = $path;
$parts = explode('/', $path);
if ($parts[2] === 'files_versions') {
$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
$length = strrpos($realPath, '.');
$realPath = substr($realPath, 0, $length);
}
return $realPath;
}
/**
* remove .part file extension and the ocTransferId from the file to get the
* original file name
* Resolves the real file path from a versioned file path.
* If the provided path points to a versioned file, this method reconstructs
* the path to its corresponding original file and strips any version suffix.
*
* @param string $path
* @return string
* @param string $path The (possibly versioned) file path.
* @return string The canonical file path.
*/
protected function stripPartFileExtension($path) {
if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
$pos = strrpos($path, '.', -6);
$path = substr($path, 0, $pos);
}
protected function getPathToRealFile(string $path): string {
$parts = explode('/', $path);
// Robustness: ensure the path has enough segments for version detection
if (count($parts) > 2 && $parts[2] === 'files_versions') {
// Reconstruct the original file path
$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
// Remove the file version suffix if present (e.g., .v123)
$dotPos = strrpos($realPath, '.');
if ($dotPos !== false) {
return substr($realPath, 0, $dotPos);
}
return $realPath;
}
// If not a versioned path, return as is
return $path;
}
/**
* get owner of a file
* Retrieves the owner of a file, caching the result for future lookups.
*
* @param string $path
* @return string
* @param string $path Path to the file.
* @return string Owner of the file.
*/
protected function getOwner($path) {
protected function getOwner(string $path): string {
if (!isset($this->owner[$path])) {
$this->owner[$path] = $this->util->getOwner($path);
}
@@ -527,16 +692,14 @@ class Encryption implements IEncryptionModule {
}
/**
* 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.
* Checks if the module is ready to be used by the specified user.
* Returns false if key pairs have not been generated for the user.
*
* @param string $user
* @return boolean
* @param string $user User to check.
* @return bool True if ready, false otherwise.
* @since 9.1.0
*/
public function isReadyForUser($user) {
public function isReadyForUser(string $user): bool {
if ($this->util->isMasterKeyEnabled()) {
return true;
}
@@ -544,11 +707,12 @@ class Encryption implements IEncryptionModule {
}
/**
* We only need a detailed access list if the master key is not enabled
* Indicates whether the encryption module needs a detailed list of users with access to the file.
* For example, modules using per-user encryption keys may require this information.
*
* @return bool
* @return bool True if a detailed access list is required, false otherwise.
*/
public function needDetailedAccessList() {
public function needDetailedAccessList(): bool {
return !$this->util->isMasterKeyEnabled();
}
}

View File

@@ -618,7 +618,7 @@ class KeyManager {
}
/**
* add system keys such as the public share key and the recovery key
* add system keys such as the public link share key and the recovery key
*
* @param array $accessList
* @param array $publicKeys
@@ -627,6 +627,7 @@ class KeyManager {
* @throws PublicKeyMissingException
*/
public function addSystemKeys(array $accessList, array $publicKeys, $uid) {
// Only add if already shared
if (!empty($accessList['public'])) {
$publicShareKey = $this->getPublicShareKey();
if (empty($publicShareKey)) {
@@ -635,6 +636,7 @@ class KeyManager {
$publicKeys[$this->getPublicShareKeyId()] = $publicShareKey;
}
// Only add if recovery key is applicable
if ($this->recoveryKeyExists()
&& $this->util->isRecoveryEnabledForUser($uid)) {
$publicKeys[$this->getRecoveryKeyId()] = $this->getRecoveryKey();

View File

@@ -1,38 +0,0 @@
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
describe('OCP.Comments tests', function() {
function dataProvider() {
return [
{ input: 'nextcloud.com', expected: 'nextcloud.com' },
{ input: 'http://nextcloud.com', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a>' },
{ input: 'https://nextcloud.com', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a>' },
{ input: 'hi nextcloud.com', expected: 'hi nextcloud.com' },
{ input: 'hi http://nextcloud.com', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a>' },
{ input: 'hi https://nextcloud.com', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a>' },
{ input: 'nextcloud.com foobar', expected: 'nextcloud.com foobar' },
{ input: 'http://nextcloud.com foobar', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a> foobar' },
{ input: 'https://nextcloud.com foobar', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a> foobar' },
{ input: 'hi nextcloud.com foobar', expected: 'hi nextcloud.com foobar' },
{ input: 'hi http://nextcloud.com foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a> foobar' },
{ input: 'hi https://nextcloud.com foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a> foobar' },
{ input: 'hi help.nextcloud.com/category/topic foobar', expected: 'hi help.nextcloud.com/category/topic foobar' },
{ input: 'hi http://help.nextcloud.com/category/topic foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="http://help.nextcloud.com/category/topic">http://help.nextcloud.com/category/topic</a> foobar' },
{ input: 'hi https://help.nextcloud.com/category/topic foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="https://help.nextcloud.com/category/topic">help.nextcloud.com/category/topic</a> foobar' },
{ input: 'noreply@nextcloud.com', expected: 'noreply@nextcloud.com' },
{ input: 'hi noreply@nextcloud.com', expected: 'hi noreply@nextcloud.com' },
{ input: 'hi <noreply@nextcloud.com>', expected: 'hi <noreply@nextcloud.com>' },
{ input: 'FirebaseInstanceId.getInstance().deleteInstanceId()', expected: 'FirebaseInstanceId.getInstance().deleteInstanceId()' },
{ input: 'I mean...it', expected: 'I mean...it' },
]
}
it('should parse URLs only', function() {
dataProvider().forEach(function(data) {
const result = OCP.Comments.plainToRich(data.input)
expect(result).toEqual(data.expected)
})
})
})

View File

@@ -1,76 +0,0 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import LoginForm from './LoginForm.vue'
describe('core: LoginForm', { testIsolation: true }, () => {
beforeEach(() => {
// Mock the required global state
cy.window().then(($window) => {
$window.OC = {
theme: {
name: 'J\'s cloud',
},
requestToken: 'request-token',
}
})
})
/**
* Ensure that characters like ' are not double HTML escaped.
* This was a bug in https://github.com/nextcloud/server/issues/34990
*/
it('does not double escape special characters in product name', () => {
cy.mount(LoginForm, {
propsData: {
username: 'test-user',
},
})
cy.get('h2').contains('J\'s cloud')
})
it('fills username from props into form', () => {
cy.mount(LoginForm, {
propsData: {
username: 'test-user',
},
})
cy.get('input[name="user"]')
.should('exist')
.and('have.attr', 'id', 'user')
cy.get('input[name="user"]')
.should('have.value', 'test-user')
})
it('clears password after timeout', () => {
// mock timeout of 5 seconds
cy.window().then(($window) => {
const state = $window.document.createElement('input')
state.type = 'hidden'
state.id = 'initial-state-core-loginTimeout'
state.value = btoa(JSON.stringify(5))
$window.document.body.appendChild(state)
})
// mount forms
cy.mount(LoginForm)
cy.get('input[name="password"]')
.should('exist')
.type('MyPassword')
cy.get('input[name="password"]')
.should('have.value', 'MyPassword')
// Wait for timeout
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5100)
cy.get('input[name="password"]')
.should('have.value', '')
})
})

View File

@@ -194,10 +194,10 @@ export default {
}
},
data() {
data(props) {
return {
loading: false,
user: '',
user: props.username,
password: '',
}
},
@@ -262,7 +262,7 @@ export default {
},
emailEnabled() {
return this.emailStates ? this.emailStates.every((state) => state === '1') : 1
return this.emailStates.every((state) => state === '1')
},
loginText() {
@@ -286,7 +286,6 @@ export default {
if (this.username === '') {
this.$refs.user.$refs.inputField.$refs.input.focus()
} else {
this.user = this.username
this.$refs.password.$refs.inputField.$refs.input.focus()
}
},

View File

@@ -0,0 +1,33 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { expect, it } from 'vitest'
import * as Comments from '../../OCP/comments.js'
it.for([
{ input: 'nextcloud.com', expected: 'nextcloud.com' },
{ input: 'http://nextcloud.com', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a>' },
{ input: 'https://nextcloud.com', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a>' },
{ input: 'hi nextcloud.com', expected: 'hi nextcloud.com' },
{ input: 'hi http://nextcloud.com', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a>' },
{ input: 'hi https://nextcloud.com', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a>' },
{ input: 'nextcloud.com foobar', expected: 'nextcloud.com foobar' },
{ input: 'http://nextcloud.com foobar', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a> foobar' },
{ input: 'https://nextcloud.com foobar', expected: '<a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a> foobar' },
{ input: 'hi nextcloud.com foobar', expected: 'hi nextcloud.com foobar' },
{ input: 'hi http://nextcloud.com foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="http://nextcloud.com">http://nextcloud.com</a> foobar' },
{ input: 'hi https://nextcloud.com foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="https://nextcloud.com">nextcloud.com</a> foobar' },
{ input: 'hi help.nextcloud.com/category/topic foobar', expected: 'hi help.nextcloud.com/category/topic foobar' },
{ input: 'hi http://help.nextcloud.com/category/topic foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="http://help.nextcloud.com/category/topic">http://help.nextcloud.com/category/topic</a> foobar' },
{ input: 'hi https://help.nextcloud.com/category/topic foobar', expected: 'hi <a class="external" target="_blank" rel="noopener noreferrer" href="https://help.nextcloud.com/category/topic">help.nextcloud.com/category/topic</a> foobar' },
{ input: 'noreply@nextcloud.com', expected: 'noreply@nextcloud.com' },
{ input: 'hi noreply@nextcloud.com', expected: 'hi noreply@nextcloud.com' },
{ input: 'hi <noreply@nextcloud.com>', expected: 'hi <noreply@nextcloud.com>' },
{ input: 'FirebaseInstanceId.getInstance().deleteInstanceId()', expected: 'FirebaseInstanceId.getInstance().deleteInstanceId()' },
{ input: 'I mean...it', expected: 'I mean...it' },
])('OCP.Comments should parse URLs only', ({ input, expected }) => {
const result = Comments.plainToRich(input)
expect(result).toEqual(expected)
})

View File

@@ -0,0 +1,102 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { cleanup, render } from '@testing-library/vue'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import LoginForm from '../../../components/login/LoginForm.vue'
describe('core: LoginForm', () => {
afterEach(cleanup)
beforeEach(() => {
// Mock the required global state
window.OC = {
theme: {
name: 'J\'s cloud',
},
requestToken: 'request-token',
}
})
/**
* Ensure that characters like ' are not double HTML escaped.
* This was a bug in https://github.com/nextcloud/server/issues/34990
*/
it('does not double escape special characters in product name', () => {
const page = render(LoginForm, {
props: {
username: 'test-user',
},
})
const heading = page.getByRole('heading', { level: 2 })
expect(heading.textContent).toContain('J\'s cloud')
})
it('offers email as login name by default', async () => {
const page = render(LoginForm)
const input = await page.findByRole('textbox', { name: /Account name or email/ })
expect(input).toBeInstanceOf(HTMLInputElement)
})
it('offers only account name if email is not enabled', async () => {
const page = render(LoginForm, {
propsData: {
emailStates: ['0', '1'],
},
})
await expect(async () => page.findByRole('textbox', { name: /Account name or email/ })).rejects.toThrow()
await expect(page.findByRole('textbox', { name: /Account name/ })).resolves.not.toThrow()
})
it('fills username from props into form', () => {
const page = render(LoginForm, {
props: {
username: 'test-user',
},
})
page.debug()
const input: HTMLInputElement = page.getByRole('textbox', { name: /Account name or email/ })
expect(input.id).toBe('user')
expect(input.name).toBe('user')
expect(input.value).toBe('test-user')
})
describe('', () => {
beforeAll(() => {
vi.useFakeTimers()
// mock timeout of 5 seconds
const state = document.createElement('input')
state.type = 'hidden'
state.id = 'initial-state-core-loginTimeout'
state.value = btoa(JSON.stringify(5))
document.body.appendChild(state)
})
afterAll(() => {
vi.useRealTimers()
document.querySelector('#initial-state-core-loginTimeout')?.remove()
})
it('clears password after timeout', () => {
// mount forms
const page = render(LoginForm)
const input: HTMLInputElement = page.getByLabelText('Password', { selector: 'input' })
input.dispatchEvent(new InputEvent('input', { data: 'MyPassword' }))
vi.advanceTimersByTime(2500)
// see its still the value
expect(input.value).toBe('')
// Wait for timeout
vi.advanceTimersByTime(2600)
expect(input.value).toBe('')
})
})
})

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -63,6 +63,8 @@ export default defineConfig([
rules: {
'no-console': 'off',
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-param-description': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
},

View File

@@ -11,165 +11,159 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Interface IEncryptionModule
* Interface for encryption modules for use by Nextcloud's Server-side Encryption (SSE).
*
* This interface allows Nextcloud SSE to support multiple interchangeable encryption modules.
*
* @since 8.1.0
*/
interface IEncryptionModule {
/**
* @return string defining the technical unique id
* Returns the unique technical identifier for this encryption module.
* This ID must be globally unique among all modules and is used for internal reference.
*
* @return string The technical module ID.
* @since 8.1.0
*/
public function getId();
public function getId(): string;
/**
* In comparison to getKey() this function returns a human readable (maybe translated) name
* Returns a human-readable (and possibly translated) name for the encryption module.
* Intended for display in user interfaces and logs.
*
* @return string
* @return string The display name of the module.
* @since 8.1.0
*/
public function getDisplayName();
public function getDisplayName(): string;
/**
* start receiving chunks from a file. This is the place where you can
* perform some initial step before starting encrypting/decrypting the
* chunks
* Initializes the encryption or decryption process for a file.
* Called before chunked processing begins. Allows the module to prepare state,
* analyze headers, and determine access control.
*
* @param string $path to the file
* @param string $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
* @param string $path The path to the file.
* @param string $user The user performing the operation (or null for public access).
* @param string $mode The PHP stream open mode (e.g., 'r', 'w').
* @param array $header Header data read from the file.
* @param array $accessList Access control list; must include keys 'users' and 'public'.
* @return array Key-value pairs for header data to write (if writing), or an empty array if not needed.
* @since 8.1.0
*/
public function begin($path, $user, $mode, array $header, array $accessList);
public function begin(string $path, string $user, string $mode, array $header, array $accessList): array;
/**
* 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 id of the last block (looks like "<Number>end")
*
* @return string remained data which should be written to the file in case
* of a write operation
* Finalizes the encryption or decryption process for a file.
* Called after all chunks have been processed. Allows the module to flush buffers and perform cleanup.
*
* @param string $path The path to the file.
* @param string $position The identifier of the last block (e.g., "<Number>end").
* @return string Any remaining data to write at the end of a write operation (empty string if none).
* @since 8.1.0
* @since 9.0.0 parameter $position added
* @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')
*
* @return mixed encrypted data
* Encrypts a chunk of file data.
*
* @param string $data The plaintext data to encrypt.
* @param string $position The position or identifier of the chunk/block (typically starts at '0').
* @return string The encrypted data for this chunk.
* @since 8.1.0
* @since 9.0.0 parameter $position added
* @since 9.0.0 Parameter $position added.
*/
public function encrypt($data, $position);
public function encrypt(string $data, string $position): string;
/**
* decrypt data
*
* @param string $data you want to decrypt
* @param int|string $position position of the block we want to decrypt
*
* @return mixed decrypted data
* Decrypts a chunk of file data.
*
* @param string $data The encrypted data to decrypt.
* @param int|string $position The position or identifier of the chunk/block.
* @return string The decrypted (plaintext) data for this chunk.
* @since 8.1.0
* @since 9.0.0 parameter $position added
* @since 9.0.0 Parameter $position added.
*/
public function decrypt($data, $position);
public function decrypt(string $data, int|string $position): string;
/**
* update encrypted file, e.g. give additional users access to the file
* Updates the encryption metadata for a file.
* For example, grants additional users access to the file or updates access lists.
*
* @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
* @param string $path Path to the file to update.
* @param string $uid The user performing the update.
* @param array $accessList Updated access control list; must include keys 'users' and 'public'.
* @return bool True on success, false otherwise.
* @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
* Checks whether the file at the given path should be encrypted by this module.
*
* @param string $path
* @return boolean
* @param string $path Path to the file.
* @return bool True if the file should be encrypted, false otherwise.
* @since 8.1.0
*/
public function shouldEncrypt($path);
public function shouldEncrypt(string $path): bool;
/**
* get size of the unencrypted payload per block.
* ownCloud read/write files with a block size of 8192 byte
* Returns the size (in bytes) of each unencrypted block payload.
* Nextcloud reads/writes files using blocks of 8192 bytes.
*
* @param bool $signed
* @return int
* @since 8.1.0 optional parameter $signed was added in 9.0.0
* @param bool $signed True if the block is signed; affects the available payload size.
* @return int The size of the unencrypted block payload in bytes.
* @since 8.1.0 (optional parameter $signed 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,
* e.g. if all encryption keys exists
* Checks if this module can decrypt and read the given file for the specified user.
* For example, verifies that all necessary encryption keys exist for this user.
*
* @param string $path
* @param string $uid user for whom we want to check if they can read the file
* @return boolean
* @param string $path Path to the file.
* @param string $uid User for whom to check readability.
* @return bool True if the file can be read, false otherwise.
* @since 8.1.0
*/
public function isReadable($path, $uid);
public function isReadable(string $path, string $uid): bool;
/**
* Initial encryption of all files
* Performs initial encryption of all files (bulk operation).
* Used for server-side bulk encryption, typically from command-line tools.
*
* @param InputInterface $input
* @param OutputInterface $output write some status information to the terminal during encryption
* @param InputInterface $input Input interface.
* @param OutputInterface $output Output interface for status/progress information.
* @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
* Prepares the encryption module for a bulk decrypt-all operation.
*
* @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
* @return bool return false on failure or if it isn't supported by the module
* @param InputInterface $input Input interface.
* @param OutputInterface $output Output interface for status/progress information.
* @param string $user (Optional) User for whom files should be decrypted. If omitted, decrypts for all users.
* @return bool True on success, false if the operation is not supported or failed.
* @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.
* 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.
* Checks if the module is ready to be used by the specified user.
* For example, returns false if key pairs have not yet been generated for the user.
*
* @param string $user
* @return boolean
* @param string $user User to check.
* @return bool True if ready, false otherwise.
* @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?
* 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.
* Indicates whether this module requires a detailed list of users with access to each file.
* For example, modules using per-user encryption keys may require this information.
*
* @return bool True if a detailed access list is required, false otherwise.
* @since 13.0.0
* @return bool
*/
public function needDetailedAccessList();
public function needDetailedAccessList(): bool;
}

2
package-lock.json generated
View File

@@ -104,7 +104,7 @@
"@testing-library/cypress": "^10.1.0",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^5.8.3",
"@testing-library/vue": "^5.9.0",
"@types/dockerode": "^3.3.44",
"@types/wait-on": "^5.3.4",
"@vitejs/plugin-vue2": "^2.3.3",

View File

@@ -141,7 +141,7 @@
"@testing-library/cypress": "^10.1.0",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^5.8.3",
"@testing-library/vue": "^5.9.0",
"@types/dockerode": "^3.3.44",
"@types/wait-on": "^5.3.4",
"@vitejs/plugin-vue2": "^2.3.3",