Skip to content

Commit

Permalink
re-encode password stored in oc's "v2" format
Browse files Browse the repository at this point in the history
Signed-off-by: Robin Appelman <[email protected]>
  • Loading branch information
icewind1991 committed Jul 27, 2023
1 parent 5abd94a commit 5138cc3
Showing 1 changed file with 112 additions and 6 deletions.
118 changes: 112 additions & 6 deletions apps/files_external/lib/Command/MigrateOc.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,45 @@

use OC\Core\Command\Base;
use OCA\Files_External\Lib\Storage\SMB;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Security\ICrypto;
use phpseclib\Crypt\AES;
use phpseclib\Crypt\Hash;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class MigrateOc extends Base {
private IDBConnection $connection;
private IConfig $config;
private ICrypto $crypto;

public const ALL = -1;

public function __construct(
IDBConnection $connection
IDBConnection $connection,
IConfig $config,
ICrypto $crypto,
) {
parent::__construct();
$this->connection = $connection;
$this->config = $config;
$this->crypto = $crypto;
}

protected function configure(): void {
$this
->setName('files_external:migrate-oc')
->setDescription('Migrate external storages when moving from ownCloud');
->setDescription('Migrate external storages when moving from ownCloud')
->addOption("dry-run", null, InputOption::VALUE_NONE, "Don't save any modifications, only try the migration");
parent::configure();
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$configs = $this->getWndConfigs();
$dryRun = $input->getOption('dry-run');

$output->writeln("Found <info>" . count($configs) . "</info> wnd storages");

Expand All @@ -76,18 +90,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$storage = new SMB($config);
$storageId = $storage->getId();
if (!$this->setStorageId($wndStorageId, $storageId)) {
$output->writeln("<error>No WMD storage with id $wndStorageId found</error>");
return 1;
if (!$dryRun) {
if (!$this->setStorageId($wndStorageId, $storageId)) {
$output->writeln("<error>No WMD storage with id $wndStorageId found</error>");
return 1;
}
}
}

if (count($configs)) {
if (count($configs) && !$dryRun) {
$this->migrateWndBackend();

$output->writeln("Successfully migrated");
}

$passwords = $this->getV2StoragePasswords();

if (count($passwords)) {
$output->writeln("Found <info>" . count($passwords) . "</info> stored passwords that need re-encoding");
foreach ($passwords as $id => $password) {
$decoded = $this->decodePassword($password);
if (!$dryRun) {
$this->setStorageConfig($id, $this->encryptPassword($decoded));
}
}
}

return 0;
}

Expand Down Expand Up @@ -121,11 +149,89 @@ private function getWndConfigs(): array {
return $configs;
}

/**
* @return array<int, string>
*/
private function getV2StoragePasswords(): array {
$query = $this->connection->getQueryBuilder();
$query->select('config_id', 'value')
->from('external_config')
->where($query->expr()->eq('key', $query->createNamedParameter('password')))
->andWhere($query->expr()->like('value', $query->createNamedParameter('v2|%')));

$rows = $query->executeQuery()->fetchAll();
$configs = [];
foreach ($rows as $row) {
$configs[(int)$row['config_id']] = $row['value'];
}
return $configs;
}

private function setStorageConfig(int $id, string $value) {
$query = $this->connection->getQueryBuilder();
$query->update('external_config')
->set('value', $query->createNamedParameter($value))
->where($query->expr()->eq('config_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
$query->executeStatement();
}

private function setStorageId(string $old, string $new): bool {
$query = $this->connection->getQueryBuilder();
$query->update('storages')
->set('id', $query->createNamedParameter($new))
->where($query->expr()->eq('id', $query->createNamedParameter($old)));
return $query->executeStatement() > 0;
}

/**
* Decrypt a password from the ownCloud scheme
*
* @param string $encoded
* @return string
* @throws \Exception
*/
private function decodePassword(string $encoded): string {
if (str_starts_with($encoded, 'v2')) {
// see https://github.com/owncloud/core/blob/89c5c364b8fa39b011c89fbfad779b547a333a92/lib/private/Security/Crypto.php#L129
$parts = \explode('|', $encoded);
$cipher = new AES();
$password = $this->config->getSystemValue('secret');
$derived = \hash_hkdf('sha512', $password, 0);
[$password, $hmacKey] = \str_split($derived, 32);
$cipher->setPassword($password);

$ciphertext = \hex2bin($parts[1]);
$iv = \hex2bin($parts[2]);
$hmac = \hex2bin($parts[3]);

$cipher->setIV($iv);

if (!\hash_equals($this->calculateHMAC($parts[1] . $iv, $hmacKey), $hmac)) {
throw new \Exception('HMAC does not match while attempting to re-encode password.');
}

return $cipher->decrypt($ciphertext);
} else {
return $this->crypto->decrypt($encoded);
}
}

private function calculateHMAC($message, $password) {
// Append an "a" behind the password and hash it to prevent reusing the same password as for encryption
$password = \hash('sha512', $password . 'a');

$hash = new Hash('sha512');
$hash->setKey($password);
return $hash->hash($message);
}

/**
* Encrypt a password in the Nextcloud scheme
*
* @param $password
* @return string
*/
private function encryptPassword($password) {
return $this->crypto->encrypt($password);
}
}

0 comments on commit 5138cc3

Please sign in to comment.