Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty
Submodule 3rdparty updated 357 files
4 changes: 1 addition & 3 deletions apps/encryption/lib/Crypto/Crypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUserSession;
use phpseclib\Crypt\RC4;
use phpseclib3\Crypt\RC4;
use Psr\Log\LoggerInterface;

/**
Expand Down Expand Up @@ -722,7 +722,6 @@ public function useLegacyBase64Encoding(): bool {
*/
private function rc4Decrypt(string $data, string $secret): string {
$rc4 = new RC4();
/** @psalm-suppress InternalMethod */
$rc4->setKey($secret);

return $rc4->decrypt($data);
Expand All @@ -733,7 +732,6 @@ private function rc4Decrypt(string $data, string $secret): string {
*/
private function rc4Encrypt(string $data, string $secret): string {
$rc4 = new RC4();
/** @psalm-suppress InternalMethod */
$rc4->setKey($secret);

return $rc4->encrypt($data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
'OCA\\Files_External\\Lib\\Storage\\OwnCloud' => $baseDir . '/../lib/Lib/Storage/OwnCloud.php',
'OCA\\Files_External\\Lib\\Storage\\SFTP' => $baseDir . '/../lib/Lib/Storage/SFTP.php',
'OCA\\Files_External\\Lib\\Storage\\SFTPReadStream' => $baseDir . '/../lib/Lib/Storage/SFTPReadStream.php',
'OCA\\Files_External\\Lib\\Storage\\SFTPReflection' => $baseDir . '/../lib/Lib/Storage/SFTPReflection.php',
'OCA\\Files_External\\Lib\\Storage\\SFTPWriteStream' => $baseDir . '/../lib/Lib/Storage/SFTPWriteStream.php',
'OCA\\Files_External\\Lib\\Storage\\SMB' => $baseDir . '/../lib/Lib/Storage/SMB.php',
'OCA\\Files_External\\Lib\\Storage\\StreamWrapper' => $baseDir . '/../lib/Lib/Storage/StreamWrapper.php',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class ComposerStaticInitFiles_External
'OCA\\Files_External\\Lib\\Storage\\OwnCloud' => __DIR__ . '/..' . '/../lib/Lib/Storage/OwnCloud.php',
'OCA\\Files_External\\Lib\\Storage\\SFTP' => __DIR__ . '/..' . '/../lib/Lib/Storage/SFTP.php',
'OCA\\Files_External\\Lib\\Storage\\SFTPReadStream' => __DIR__ . '/..' . '/../lib/Lib/Storage/SFTPReadStream.php',
'OCA\\Files_External\\Lib\\Storage\\SFTPReflection' => __DIR__ . '/..' . '/../lib/Lib/Storage/SFTPReflection.php',
'OCA\\Files_External\\Lib\\Storage\\SFTPWriteStream' => __DIR__ . '/..' . '/../lib/Lib/Storage/SFTPWriteStream.php',
'OCA\\Files_External\\Lib\\Storage\\SMB' => __DIR__ . '/..' . '/../lib/Lib/Storage/SMB.php',
'OCA\\Files_External\\Lib\\Storage\\StreamWrapper' => __DIR__ . '/..' . '/../lib/Lib/Storage/StreamWrapper.php',
Expand Down
18 changes: 10 additions & 8 deletions apps/files_external/lib/Controller/AjaxController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,15 @@ public function getApplicableEntities(string $pattern = '', ?int $limit = null,
*/
private function generateSshKeys($keyLength) {
$key = $this->rsaMechanism->createKey($keyLength);
// Replace the placeholder label with a more meaningful one
$key['publickey'] = str_replace('phpseclib-generated-key', gethostname(), $key['publickey']);

return $key;
return [
'private_key' => $key->toString('PKCS1'),
// Replace the placeholder label with a more meaningful one
'public_key' => str_replace(
'phpseclib-generated-key',
gethostname(),
$key->getPublicKey()->toString('OpenSSH'),
),
];
}

/**
Expand All @@ -93,10 +98,7 @@ private function generateSshKeys($keyLength) {
public function getSshKeys($keyLength = 1024) {
$key = $this->generateSshKeys($keyLength);
return new JSONResponse([
'data' => [
'private_key' => $key['privatekey'],
'public_key' => $key['publickey']
],
'data' => $key,
'status' => 'success',
]);
}
Expand Down
28 changes: 13 additions & 15 deletions apps/files_external/lib/Lib/Auth/PublicKey/RSA.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUser;
use phpseclib\Crypt\RSA as RSACrypt;
use phpseclib3\Crypt\RSA as RSACrypt;

/**
* RSA public key authentication
Expand Down Expand Up @@ -45,33 +45,31 @@ public function __construct(
*/
#[\Override]
public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
$auth = new RSACrypt();
$auth->setPassword($this->config->getSystemValue('secret', ''));
if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
try {
$auth = RSACrypt::loadPrivateKey(
Comment thread
susnux marked this conversation as resolved.
$storage->getBackendOption('private_key'),
$this->config->getSystemValue('secret', '')
);
} catch (\Throwable) {
// Add fallback routine for a time where secret was not enforced to be exists
$auth->setPassword('');
if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
throw new \RuntimeException('unable to load private key');
}
$auth = RSACrypt::loadPrivateKey($storage->getBackendOption('private_key'));
}

$storage->setBackendOption('public_key_auth', $auth);
}

/**
* Generate a keypair
*
* @param int $keyLenth
* @return array ['privatekey' => $privateKey, 'publickey' => $publicKey]
*/
public function createKey($keyLength) {
$rsa = new RSACrypt();
$rsa->setPublicKeyFormat(RSACrypt::PUBLIC_FORMAT_OPENSSH);
$rsa->setPassword($this->config->getSystemValue('secret', ''));

public function createKey($keyLength): RSACrypt\PrivateKey {
if ($keyLength !== 1024 && $keyLength !== 2048 && $keyLength !== 4096) {
$keyLength = 1024;
}

return $rsa->createKey($keyLength);
$secret = $this->config->getSystemValue('secret', '');
$rsa = RSACrypt::createKey($keyLength);
return $rsa->withPassword($secret);
}
}
21 changes: 13 additions & 8 deletions apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUser;
use phpseclib\Crypt\RSA as RSACrypt;
use phpseclib3\Crypt\RSA;
use phpseclib3\Exception\NoKeyLoadedException;

/**
* RSA public key authentication
Expand Down Expand Up @@ -42,14 +43,18 @@ public function __construct(
*/
#[\Override]
public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
$auth = new RSACrypt();
$auth->setPassword($this->config->getSystemValue('secret', ''));
if (!$auth->loadKey($storage->getBackendOption('private_key'))) {

try {
$auth = RSA\PrivateKey::loadPrivateKey(
$storage->getBackendOption('private_key'),
$this->config->getSystemValue('secret', ''),
);
} catch (NoKeyLoadedException) {
// Add fallback routine for a time where secret was not enforced to be exists
$auth->setPassword('');
if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
throw new \RuntimeException('unable to load private key');
}
$auth = RSA\PrivateKey::loadPrivateKey(
$storage->getBackendOption('private_key'),
'',
);
}
$storage->setBackendOption('public_key_auth', $auth);
}
Expand Down
17 changes: 8 additions & 9 deletions apps/files_external/lib/Lib/Storage/SFTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
use OCP\Server;
use phpseclib\Net\SFTP\Stream;
use phpseclib3\Net\SFTP\Stream;

/**
* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
Expand All @@ -34,7 +34,7 @@ class SFTP extends Common {
private $auth = [];

/**
* @var \phpseclib\Net\SFTP
* @var \phpseclib3\Net\SFTP
*/
protected $client;
private CappedMemoryCache $knownMTimes;
Expand Down Expand Up @@ -106,16 +106,16 @@ public function __construct(array $parameters) {
/**
* Returns the connection.
*
* @return \phpseclib\Net\SFTP connected client instance
* @return \phpseclib3\Net\SFTP connected client instance
* @throws \Exception when the connection failed
*/
public function getConnection(): \phpseclib\Net\SFTP {
public function getConnection(): \phpseclib3\Net\SFTP {
if (!is_null($this->client)) {
return $this->client;
}

$hostKeys = $this->readHostKeys();
$this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
$this->client = new \phpseclib3\Net\SFTP($this->host, $this->port);

// The SSH Host Key MUST be verified before login().
$currentHostKey = $this->client->getServerPublicHostKey();
Expand All @@ -130,7 +130,6 @@ public function getConnection(): \phpseclib\Net\SFTP {

$login = false;
foreach ($this->auth as $auth) {
/** @psalm-suppress TooManyArguments */
$login = $this->client->login($this->user, $auth);
if ($login === true) {
break;
Expand Down Expand Up @@ -338,7 +337,7 @@ public function fopen(string $path, string $mode) {
case 'wb':
SFTPWriteStream::register();
// the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache.
$connection->_remove_from_stat_cache($absPath);
$connection->clearStatCache();
$context = stream_context_create(['sftp' => ['session' => $connection]]);
$fh = fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context);
if ($fh) {
Expand Down Expand Up @@ -487,7 +486,7 @@ public function copy(string $source, string $target): bool {
$absTarget = $this->absPath($target);

$connection = $this->getConnection();
$size = $connection->size($absSource);
$size = $connection->filesize($absSource);
if ($size === false) {
return false;
}
Expand All @@ -498,7 +497,7 @@ public function copy(string $source, string $target): bool {
return false;
}
/** @psalm-suppress InternalMethod */
if (!$connection->put($absTarget, $chunk, \phpseclib\Net\SFTP::SOURCE_STRING, $i)) {
if (!$connection->put($absTarget, $chunk, \phpseclib3\Net\SFTP::SOURCE_STRING, $i)) {
return false;
}
}
Expand Down
39 changes: 22 additions & 17 deletions apps/files_external/lib/Lib/Storage/SFTPReadStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
namespace OCA\Files_External\Lib\Storage;

use Icewind\Streams\File;
use phpseclib\Net\SSH2;
use phpseclib3\Net\SSH2;

class SFTPReadStream implements File {
use SFTPReflection;

/** @var resource */
public $context;

/** @var \phpseclib\Net\SFTP */
/** @var \phpseclib3\Net\SFTP */
private $sftp;

/** @var string */
Expand Down Expand Up @@ -54,7 +56,7 @@ protected function loadContext(string $name) {
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
if (isset($context['session']) && $context['session'] instanceof \phpseclib\Net\SFTP) {
if (isset($context['session']) && $context['session'] instanceof \phpseclib3\Net\SFTP) {
$this->sftp = $context['session'];
} else {
throw new \BadMethodCallException('Invalid context, session not set');
Expand All @@ -73,27 +75,25 @@ public function stream_open($path, $mode, $options, &$opened_path) {

$this->loadContext('sftp');

if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
if (!($this->getSftpProperty($this->sftp, 'bitmap') & SSH2::MASK_LOGIN)) {
return false;
}

$remote_file = $this->sftp->_realpath($path);
$remote_file = $this->sftp->realpath($path);
if ($remote_file === false) {
return false;
}

$packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0);
if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$this->invokeSftp($this->sftp, 'send_sftp_packet', [NET_SFTP_OPEN, $packet]);

$response = $this->sftp->_get_sftp_packet();
switch ($this->sftp->packet_type) {
$response = $this->invokeSftp($this->sftp, 'get_sftp_packet');
switch ($this->getSftpProperty($this->sftp, 'packet_type')) {
case NET_SFTP_HANDLE:
$this->handle = substr($response, 4);
break;
case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
$this->sftp->_logError($response);
$this->invokeSftp($this->sftp, 'logError', [$response]);
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
Expand Down Expand Up @@ -152,19 +152,24 @@ public function stream_read($count) {

private function request_chunk(int $size) {
if ($this->pendingRead) {
$this->sftp->_get_sftp_packet();
$this->invokeSftp($this->sftp, 'get_sftp_packet');
}

$packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size);
$this->pendingRead = true;
return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet);
try {
$this->invokeSftp($this->sftp, 'send_sftp_packet', [NET_SFTP_READ, $packet]);
return true;
} catch (\Throwable) {
return false;
}
}

private function read_chunk() {
$this->pendingRead = false;
$response = $this->sftp->_get_sftp_packet();
$response = $this->invokeSftp($this->sftp, 'get_sftp_packet');

switch ($this->sftp->packet_type) {
switch ($this->getSftpProperty($this->sftp, 'packet_type')) {
case NET_SFTP_DATA:
$temp = substr($response, 4);
$len = strlen($temp);
Expand Down Expand Up @@ -220,9 +225,9 @@ public function stream_eof() {
public function stream_close() {
// we still have a read request incoming that needs to be handled before we can close
if ($this->pendingRead) {
$this->sftp->_get_sftp_packet();
$this->invokeSftp($this->sftp, 'get_sftp_packet');
}
if (!$this->sftp->_close_handle($this->handle)) {
if (!$this->invokeSftp($this->sftp, 'close_handle', [$this->handle])) {
return false;
}
return true;
Expand Down
47 changes: 47 additions & 0 deletions apps/files_external/lib/Lib/Storage/SFTPReflection.php

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any options on this solution?
Should we keep the "performance" streams with reflection or drop them completely and just use the library streams?

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_External\Lib\Storage;

use phpseclib3\Net\SFTP;

/**
* The SFTP read/write stream wrappers implement pipelined reads and buffered
* writes directly on the SFTP protocol to get better throughput than the public
* SFTP::get()/put() API can offer (a single kept-open remote handle, read-ahead,
* chunked writes).
*
* phpseclib v3 made the required low-level packet methods and a couple of
* properties private/protected. To keep the performance-oriented implementation
* working without vendoring or forking phpseclib, we reach into those internals
* through reflection. The reflection objects are cached because they are used
* once per protocol packet.
*/
trait SFTPReflection {
/** @var array<string, \ReflectionMethod> */
private static array $sftpReflectionMethods = [];
/** @var array<string, \ReflectionProperty> */
private static array $sftpReflectionProperties = [];

/**
* Invoke a method that is private in phpseclib v3.
*/
private function invokeSftp(SFTP $sftp, string $method, array $arguments = []): mixed {
self::$sftpReflectionMethods[$method] ??= new \ReflectionMethod(SFTP::class, $method);
return self::$sftpReflectionMethods[$method]->invokeArgs($sftp, $arguments);
}

/**
* Read a property that is private/protected in phpseclib v3.
*/
private function getSftpProperty(SFTP $sftp, string $property): mixed {
self::$sftpReflectionProperties[$property] ??= new \ReflectionProperty(SFTP::class, $property);
return self::$sftpReflectionProperties[$property]->getValue($sftp);
}
}
Loading
Loading