<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OCA\Encryption\Tests;

use OC\Files\FileInfo;
use OC\Files\View;
use OCA\Encryption\Crypto\Crypt;
use OCA\Encryption\Crypto\Encryption;
use OCA\Encryption\Exceptions\PrivateKeyMissingException;
use OCA\Encryption\Exceptions\PublicKeyMissingException;
use OCA\Encryption\KeyManager;
use OCA\Encryption\Session;
use OCA\Encryption\Util;
use OCP\Encryption\Keys\IStorage;
use OCP\Files\Cache\ICache;
use OCP\Files\Storage\IStorage as FilesIStorage;
use OCP\IConfig;
use OCP\IUserSession;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class KeyManagerTest extends TestCase {
	/**
	 * @var KeyManager
	 */
	private $instance;
	/**
	 * @var string
	 */
	private $userId;

	/** @var string */
	private $systemKeyId;

	/** @var \OCP\Encryption\Keys\IStorage|\PHPUnit\Framework\MockObject\MockObject */
	private $keyStorageMock;

	/** @var Crypt|\PHPUnit\Framework\MockObject\MockObject */
	private $cryptMock;

	/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
	private $userMock;

	/** @var Session|\PHPUnit\Framework\MockObject\MockObject */
	private $sessionMock;

	/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
	private $logMock;

	/** @var Util|\PHPUnit\Framework\MockObject\MockObject */
	private $utilMock;

	/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
	private $configMock;

	/** @var ILockingProvider|MockObject */
	private $lockingProviderMock;

	protected function setUp(): void {
		parent::setUp();
		$this->userId = 'user1';
		$this->systemKeyId = 'systemKeyId';
		$this->keyStorageMock = $this->createMock(IStorage::class);
		$this->cryptMock = $this->getMockBuilder(Crypt::class)
			->disableOriginalConstructor()
			->getMock();
		$this->configMock = $this->createMock(IConfig::class);
		$this->configMock->expects($this->any())
			->method('getAppValue')
			->willReturn($this->systemKeyId);
		$this->userMock = $this->createMock(IUserSession::class);
		$this->sessionMock = $this->getMockBuilder(Session::class)
			->disableOriginalConstructor()
			->getMock();
		$this->logMock = $this->createMock(LoggerInterface::class);
		$this->utilMock = $this->getMockBuilder(Util::class)
			->disableOriginalConstructor()
			->getMock();
		$this->lockingProviderMock = $this->createMock(ILockingProvider::class);

		$this->instance = new KeyManager(
			$this->keyStorageMock,
			$this->cryptMock,
			$this->configMock,
			$this->userMock,
			$this->sessionMock,
			$this->logMock,
			$this->utilMock,
			$this->lockingProviderMock
		);
	}

	public function testDeleteShareKey(): void {
		$this->keyStorageMock->expects($this->any())
			->method('deleteFileKey')
			->with($this->equalTo('/path'), $this->equalTo('keyId.shareKey'))
			->willReturn(true);

		$this->assertTrue(
			$this->instance->deleteShareKey('/path', 'keyId')
		);
	}

	public function testGetPrivateKey(): void {
		$this->keyStorageMock->expects($this->any())
			->method('getUserKey')
			->with($this->equalTo($this->userId), $this->equalTo('privateKey'))
			->willReturn('privateKey');


		$this->assertSame('privateKey',
			$this->instance->getPrivateKey($this->userId)
		);
	}

	public function testGetPublicKey(): void {
		$this->keyStorageMock->expects($this->any())
			->method('getUserKey')
			->with($this->equalTo($this->userId), $this->equalTo('publicKey'))
			->willReturn('publicKey');


		$this->assertSame('publicKey',
			$this->instance->getPublicKey($this->userId)
		);
	}

	public function testRecoveryKeyExists(): void {
		$this->keyStorageMock->expects($this->any())
			->method('getSystemUserKey')
			->with($this->equalTo($this->systemKeyId . '.publicKey'))
			->willReturn('recoveryKey');


		$this->assertTrue($this->instance->recoveryKeyExists());
	}

	public function testCheckRecoveryKeyPassword(): void {
		$this->keyStorageMock->expects($this->any())
			->method('getSystemUserKey')
			->with($this->equalTo($this->systemKeyId . '.privateKey'))
			->willReturn('recoveryKey');
		$this->cryptMock->expects($this->any())
			->method('decryptPrivateKey')
			->with($this->equalTo('recoveryKey'), $this->equalTo('pass'))
			->willReturn('decryptedRecoveryKey');

		$this->assertTrue($this->instance->checkRecoveryPassword('pass'));
	}

	public function testSetPublicKey(): void {
		$this->keyStorageMock->expects($this->any())
			->method('setUserKey')
			->with(
				$this->equalTo($this->userId),
				$this->equalTo('publicKey'),
				$this->equalTo('key'))
			->willReturn(true);


		$this->assertTrue(
			$this->instance->setPublicKey($this->userId, 'key')
		);
	}

	public function testSetPrivateKey(): void {
		$this->keyStorageMock->expects($this->any())
			->method('setUserKey')
			->with(
				$this->equalTo($this->userId),
				$this->equalTo('privateKey'),
				$this->equalTo('key'))
			->willReturn(true);


		$this->assertTrue(
			$this->instance->setPrivateKey($this->userId, 'key')
		);
	}

	/**
	 * @dataProvider dataTestUserHasKeys
	 */
	public function testUserHasKeys($key, $expected): void {
		$this->keyStorageMock->expects($this->exactly(2))
			->method('getUserKey')
			->with($this->equalTo($this->userId), $this->anything())
			->willReturn($key);


		$this->assertSame($expected,
			$this->instance->userHasKeys($this->userId)
		);
	}

	public function dataTestUserHasKeys() {
		return [
			['key', true],
			['', false]
		];
	}


	public function testUserHasKeysMissingPrivateKey(): void {
		$this->expectException(PrivateKeyMissingException::class);

		$this->keyStorageMock->expects($this->exactly(2))
			->method('getUserKey')
			->willReturnCallback(function ($uid, $keyID, $encryptionModuleId) {
				if ($keyID === 'privateKey') {
					return '';
				}
				return 'key';
			});

		$this->instance->userHasKeys($this->userId);
	}


	public function testUserHasKeysMissingPublicKey(): void {
		$this->expectException(PublicKeyMissingException::class);

		$this->keyStorageMock->expects($this->exactly(2))
			->method('getUserKey')
			->willReturnCallback(function ($uid, $keyID, $encryptionModuleId) {
				if ($keyID === 'publicKey') {
					return '';
				}
				return 'key';
			});

		$this->instance->userHasKeys($this->userId);
	}

	/**
	 * @dataProvider dataTestInit
	 *
	 * @param bool $useMasterKey
	 */
	public function testInit($useMasterKey): void {
		/** @var KeyManager|\PHPUnit\Framework\MockObject\MockObject $instance */
		$instance = $this->getMockBuilder(KeyManager::class)
			->setConstructorArgs(
				[
					$this->keyStorageMock,
					$this->cryptMock,
					$this->configMock,
					$this->userMock,
					$this->sessionMock,
					$this->logMock,
					$this->utilMock,
					$this->lockingProviderMock
				]
			)->setMethods(['getMasterKeyId', 'getMasterKeyPassword', 'getSystemPrivateKey', 'getPrivateKey'])
			->getMock();

		$this->utilMock->expects($this->once())->method('isMasterKeyEnabled')
			->willReturn($useMasterKey);

		$this->sessionMock->expects($this->exactly(2))->method('setStatus')
			->withConsecutive(
				[Session::INIT_EXECUTED],
				[Session::INIT_SUCCESSFUL],
			);

		$instance->expects($this->any())->method('getMasterKeyId')->willReturn('masterKeyId');
		$instance->expects($this->any())->method('getMasterKeyPassword')->willReturn('masterKeyPassword');
		$instance->expects($this->any())->method('getSystemPrivateKey')->with('masterKeyId')->willReturn('privateMasterKey');
		$instance->expects($this->any())->method('getPrivateKey')->with($this->userId)->willReturn('privateUserKey');

		if ($useMasterKey) {
			$this->cryptMock->expects($this->once())->method('decryptPrivateKey')
				->with('privateMasterKey', 'masterKeyPassword', 'masterKeyId')
				->willReturn('key');
		} else {
			$this->cryptMock->expects($this->once())->method('decryptPrivateKey')
				->with('privateUserKey', 'pass', $this->userId)
				->willReturn('key');
		}

		$this->sessionMock->expects($this->once())->method('setPrivateKey')
			->with('key');

		$this->assertTrue($instance->init($this->userId, 'pass'));
	}

	public function dataTestInit() {
		return [
			[true],
			[false]
		];
	}


	public function testSetRecoveryKey(): void {
		$this->keyStorageMock->expects($this->exactly(2))
			->method('setSystemUserKey')
			->willReturn(true);
		$this->cryptMock->expects($this->any())
			->method('encryptPrivateKey')
			->with($this->equalTo('privateKey'), $this->equalTo('pass'))
			->willReturn('decryptedPrivateKey');


		$this->assertTrue(
			$this->instance->setRecoveryKey('pass',
				['publicKey' => 'publicKey', 'privateKey' => 'privateKey'])
		);
	}

	public function testSetSystemPrivateKey(): void {
		$this->keyStorageMock->expects($this->exactly(1))
			->method('setSystemUserKey')
			->with($this->equalTo('keyId.privateKey'), $this->equalTo('key'))
			->willReturn(true);


		$this->assertTrue(
			$this->instance->setSystemPrivateKey('keyId', 'key')
		);
	}

	public function testGetSystemPrivateKey(): void {
		$this->keyStorageMock->expects($this->exactly(1))
			->method('getSystemUserKey')
			->with($this->equalTo('keyId.privateKey'))
			->willReturn('systemPrivateKey');


		$this->assertSame('systemPrivateKey',
			$this->instance->getSystemPrivateKey('keyId')
		);
	}

	public function testGetEncryptedFileKey(): void {
		$this->keyStorageMock->expects($this->once())
			->method('getFileKey')
			->with('/', 'fileKey')
			->willReturn(true);

		$this->assertTrue($this->instance->getEncryptedFileKey('/'));
	}

	public function dataTestGetFileKey() {
		return [
			['user1', false, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'],
			['user1', false, 'privateKey', '', 'multiKeyDecryptResult'],
			['user1', false, false, 'legacyKey', ''],
			['user1', false, false, '', ''],
			['user1', true, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'],
			['user1', true, 'privateKey', '', 'multiKeyDecryptResult'],
			['user1', true, false, 'legacyKey', ''],
			['user1', true, false, '', ''],
			[null, false, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'],
			[null, false, 'privateKey', '', 'multiKeyDecryptResult'],
			[null, false, false, 'legacyKey', ''],
			[null, false, false, '', ''],
			[null, true, 'privateKey', 'legacyKey', 'multiKeyDecryptResult'],
			[null, true, 'privateKey', '', 'multiKeyDecryptResult'],
			[null, true, false, 'legacyKey', ''],
			[null, true, false, '', ''],
		];
	}

	/**
	 * @dataProvider dataTestGetFileKey
	 *
	 * @param $uid
	 * @param $isMasterKeyEnabled
	 * @param $privateKey
	 * @param $expected
	 */
	public function testGetFileKey($uid, $isMasterKeyEnabled, $privateKey, $encryptedFileKey, $expected): void {
		$path = '/foo.txt';

		if ($isMasterKeyEnabled) {
			$expectedUid = 'masterKeyId';
			$this->configMock->expects($this->any())->method('getSystemValue')->with('secret')
				->willReturn('password');
		} elseif (!$uid) {
			$expectedUid = 'systemKeyId';
		} else {
			$expectedUid = $uid;
		}

		$this->invokePrivate($this->instance, 'masterKeyId', ['masterKeyId']);

		$this->keyStorageMock->expects($this->exactly(2))
			->method('getFileKey')
			->withConsecutive(
				[$path, 'fileKey', 'OC_DEFAULT_MODULE'],
				[$path, $expectedUid . '.shareKey', 'OC_DEFAULT_MODULE'],
			)
			->willReturnOnConsecutiveCalls(
				$encryptedFileKey,
				'fileKey',
			);

		$this->utilMock->expects($this->any())->method('isMasterKeyEnabled')
			->willReturn($isMasterKeyEnabled);

		if (is_null($uid)) {
			$this->keyStorageMock->expects($this->once())
				->method('getSystemUserKey')
				->willReturn(true);
			$this->cryptMock->expects($this->once())
				->method('decryptPrivateKey')
				->willReturn($privateKey);
		} else {
			$this->keyStorageMock->expects($this->never())
				->method('getSystemUserKey');
			$this->sessionMock->expects($this->once())->method('getPrivateKey')->willReturn($privateKey);
		}

		if (!empty($encryptedFileKey)) {
			$this->cryptMock->expects($this->never())
				->method('multiKeyDecrypt');
			if ($privateKey) {
				$this->cryptMock->expects($this->once())
					->method('multiKeyDecryptLegacy')
					->willReturn('multiKeyDecryptResult');
			} else {
				$this->cryptMock->expects($this->never())
					->method('multiKeyDecryptLegacy');
			}
		} else {
			$this->cryptMock->expects($this->never())
				->method('multiKeyDecryptLegacy');
			if ($privateKey) {
				$this->cryptMock->expects($this->once())
					->method('multiKeyDecrypt')
					->willReturn('multiKeyDecryptResult');
			} else {
				$this->cryptMock->expects($this->never())
					->method('multiKeyDecrypt');
			}
		}

		$this->assertSame($expected,
			$this->instance->getFileKey($path, $uid, null)
		);
	}

	public function testDeletePrivateKey(): void {
		$this->keyStorageMock->expects($this->once())
			->method('deleteUserKey')
			->with('user1', 'privateKey')
			->willReturn(true);

		$this->assertTrue(self::invokePrivate($this->instance,
			'deletePrivateKey',
			[$this->userId]));
	}

	public function testDeleteAllFileKeys(): void {
		$this->keyStorageMock->expects($this->once())
			->method('deleteAllFileKeys')
			->willReturn(true);

		$this->assertTrue($this->instance->deleteAllFileKeys('/'));
	}

	/**
	 * test add public share key and or recovery key to the list of public keys
	 *
	 * @dataProvider dataTestAddSystemKeys
	 *
	 * @param array $accessList
	 * @param array $publicKeys
	 * @param string $uid
	 * @param array $expectedKeys
	 */
	public function testAddSystemKeys($accessList, $publicKeys, $uid, $expectedKeys): void {
		$publicShareKeyId = 'publicShareKey';
		$recoveryKeyId = 'recoveryKey';

		$this->keyStorageMock->expects($this->any())
			->method('getSystemUserKey')
			->willReturnCallback(function ($keyId, $encryptionModuleId) {
				return $keyId;
			});

		$this->utilMock->expects($this->any())
			->method('isRecoveryEnabledForUser')
			->willReturnCallback(function ($uid) {
				if ($uid === 'user1') {
					return true;
				}
				return false;
			});

		// set key IDs
		self::invokePrivate($this->instance, 'publicShareKeyId', [$publicShareKeyId]);
		self::invokePrivate($this->instance, 'recoveryKeyId', [$recoveryKeyId]);

		$result = $this->instance->addSystemKeys($accessList, $publicKeys, $uid);

		foreach ($expectedKeys as $expected) {
			$this->assertArrayHasKey($expected, $result);
		}

		$this->assertSameSize($expectedKeys, $result);
	}

	/**
	 * data provider for testAddSystemKeys()
	 *
	 * @return array
	 */
	public function dataTestAddSystemKeys() {
		return [
			[['public' => true],[], 'user1', ['publicShareKey', 'recoveryKey']],
			[['public' => false], [], 'user1', ['recoveryKey']],
			[['public' => true],[], 'user2', ['publicShareKey']],
			[['public' => false], [], 'user2', []],
		];
	}

	public function testGetMasterKeyId(): void {
		$this->assertSame('systemKeyId', $this->instance->getMasterKeyId());
	}

	public function testGetPublicMasterKey(): void {
		$this->keyStorageMock->expects($this->once())->method('getSystemUserKey')
			->with('systemKeyId.publicKey', Encryption::ID)
			->willReturn(true);

		$this->assertTrue(
			$this->instance->getPublicMasterKey()
		);
	}

	public function testGetMasterKeyPassword(): void {
		$this->configMock->expects($this->once())->method('getSystemValue')->with('secret')
			->willReturn('password');

		$this->assertSame('password',
			$this->invokePrivate($this->instance, 'getMasterKeyPassword', [])
		);
	}


	public function testGetMasterKeyPasswordException(): void {
		$this->expectException(\Exception::class);

		$this->configMock->expects($this->once())->method('getSystemValue')->with('secret')
			->willReturn('');

		$this->invokePrivate($this->instance, 'getMasterKeyPassword', []);
	}

	/**
	 * @dataProvider dataTestValidateMasterKey
	 *
	 * @param $masterKey
	 */
	public function testValidateMasterKey($masterKey): void {
		/** @var KeyManager|\PHPUnit\Framework\MockObject\MockObject $instance */
		$instance = $this->getMockBuilder(KeyManager::class)
			->setConstructorArgs(
				[
					$this->keyStorageMock,
					$this->cryptMock,
					$this->configMock,
					$this->userMock,
					$this->sessionMock,
					$this->logMock,
					$this->utilMock,
					$this->lockingProviderMock
				]
			)->setMethods(['getPublicMasterKey', 'setSystemPrivateKey', 'getMasterKeyPassword'])
			->getMock();

		$this->utilMock->expects($this->once())->method('isMasterKeyEnabled')
			->willReturn(true);

		$instance->expects($this->once())->method('getPublicMasterKey')
			->willReturn($masterKey);

		$instance->expects($this->any())->method('getMasterKeyPassword')->willReturn('masterKeyPassword');
		$this->cryptMock->expects($this->any())->method('generateHeader')->willReturn('header');

		if (empty($masterKey)) {
			$this->cryptMock->expects($this->once())->method('createKeyPair')
				->willReturn(['publicKey' => 'public', 'privateKey' => 'private']);
			$this->keyStorageMock->expects($this->once())->method('setSystemUserKey')
				->with('systemKeyId.publicKey', 'public', Encryption::ID);
			$this->cryptMock->expects($this->once())->method('encryptPrivateKey')
				->with('private', 'masterKeyPassword', 'systemKeyId')
				->willReturn('EncryptedKey');
			$this->lockingProviderMock->expects($this->once())
				->method('acquireLock');
			$instance->expects($this->once())->method('setSystemPrivateKey')
				->with('systemKeyId', 'headerEncryptedKey');
		} else {
			$this->cryptMock->expects($this->never())->method('createKeyPair');
			$this->keyStorageMock->expects($this->never())->method('setSystemUserKey');
			$this->cryptMock->expects($this->never())->method('encryptPrivateKey');
			$instance->expects($this->never())->method('setSystemPrivateKey');
		}

		$instance->validateMasterKey();
	}

	public function testValidateMasterKeyLocked(): void {
		/** @var KeyManager|\PHPUnit_Framework_MockObject_MockObject $instance */
		$instance = $this->getMockBuilder(KeyManager::class)
			->setConstructorArgs(
				[
					$this->keyStorageMock,
					$this->cryptMock,
					$this->configMock,
					$this->userMock,
					$this->sessionMock,
					$this->logMock,
					$this->utilMock,
					$this->lockingProviderMock
				]
			)->setMethods(['getPublicMasterKey', 'getPrivateMasterKey', 'setSystemPrivateKey', 'getMasterKeyPassword'])
			->getMock();

		$this->utilMock->expects($this->once())->method('isMasterKeyEnabled')
			->willReturn(true);

		$instance->expects($this->once())->method('getPublicMasterKey')
			->willReturn('');
		$instance->expects($this->once())->method('getPrivateMasterKey')
			->willReturn('');

		$instance->expects($this->any())->method('getMasterKeyPassword')->willReturn('masterKeyPassword');
		$this->cryptMock->expects($this->any())->method('generateHeader')->willReturn('header');

		$this->lockingProviderMock->expects($this->once())
			->method('acquireLock')
			->willThrowException(new LockedException('encryption-generateMasterKey'));

		$this->expectException(LockedException::class);
		$instance->validateMasterKey();
	}

	public function dataTestValidateMasterKey() {
		return [
			['masterKey'],
			['']
		];
	}

	public function testGetVersionWithoutFileInfo(): void {
		$view = $this->getMockBuilder(View::class)
			->disableOriginalConstructor()->getMock();
		$view->expects($this->once())
			->method('getFileInfo')
			->with('/admin/files/myfile.txt')
			->willReturn(false);

		/** @var View $view */
		$this->assertSame(0, $this->instance->getVersion('/admin/files/myfile.txt', $view));
	}

	public function testGetVersionWithFileInfo(): void {
		$view = $this->getMockBuilder(View::class)
			->disableOriginalConstructor()->getMock();
		$fileInfo = $this->getMockBuilder(FileInfo::class)
			->disableOriginalConstructor()->getMock();
		$fileInfo->expects($this->once())
			->method('getEncryptedVersion')
			->willReturn(1337);
		$view->expects($this->once())
			->method('getFileInfo')
			->with('/admin/files/myfile.txt')
			->willReturn($fileInfo);

		/** @var View $view */
		$this->assertSame(1337, $this->instance->getVersion('/admin/files/myfile.txt', $view));
	}

	public function testSetVersionWithFileInfo(): void {
		$view = $this->getMockBuilder(View::class)
			->disableOriginalConstructor()->getMock();
		$cache = $this->getMockBuilder(ICache::class)
			->disableOriginalConstructor()->getMock();
		$cache->expects($this->once())
			->method('update')
			->with(123, ['encrypted' => 5, 'encryptedVersion' => 5]);
		$storage = $this->getMockBuilder(FilesIStorage::class)
			->disableOriginalConstructor()->getMock();
		$storage->expects($this->once())
			->method('getCache')
			->willReturn($cache);
		$fileInfo = $this->getMockBuilder(FileInfo::class)
			->disableOriginalConstructor()->getMock();
		$fileInfo->expects($this->once())
			->method('getStorage')
			->willReturn($storage);
		$fileInfo->expects($this->once())
			->method('getId')
			->willReturn(123);
		$view->expects($this->once())
			->method('getFileInfo')
			->with('/admin/files/myfile.txt')
			->willReturn($fileInfo);

		/** @var View $view */
		$this->instance->setVersion('/admin/files/myfile.txt', 5, $view);
	}

	public function testSetVersionWithoutFileInfo(): void {
		$view = $this->getMockBuilder(View::class)
			->disableOriginalConstructor()->getMock();
		$view->expects($this->once())
			->method('getFileInfo')
			->with('/admin/files/myfile.txt')
			->willReturn(false);

		/** @var View $view */
		$this->instance->setVersion('/admin/files/myfile.txt', 5, $view);
	}

	public function testBackupUserKeys(): void {
		$this->keyStorageMock->expects($this->once())->method('backupUserKeys')
			->with('OC_DEFAULT_MODULE', 'test', 'user1');
		$this->instance->backupUserKeys('test', 'user1');
	}
}
