* @author Joas Schilling * @author Lukas Reschke * @author Morris Jobke * @author Roeland Jago Douma * @author Victor Dubiniuk * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ namespace OC\Session; use OCP\ISession; use OCP\Security\ICrypto; use OCP\Session\Exceptions\SessionNotAvailableException; use function json_decode; use function OCP\Log\logger; /** * Class CryptoSessionData * * @package OC\Session * @template-implements \ArrayAccess */ class CryptoSessionData implements \ArrayAccess, ISession { /** @var ISession */ protected $session; /** @var \OCP\Security\ICrypto */ protected $crypto; /** @var string */ protected $passphrase; /** @var array */ protected $sessionValues; /** @var bool */ protected $isModified = false; public const encryptedSessionName = 'encrypted_session_data'; /** * @param ISession $session * @param ICrypto $crypto * @param string $passphrase */ public function __construct(ISession $session, ICrypto $crypto, string $passphrase) { $this->crypto = $crypto; $this->session = $session; $this->passphrase = $passphrase; $this->initializeSession(); } /** * Close session if class gets destructed */ public function __destruct() { try { $this->close(); } catch (SessionNotAvailableException $e) { // This exception can occur if session is already closed // So it is safe to ignore it and let the garbage collector to proceed } } protected function initializeSession() { $encryptedSessionData = $this->session->get(self::encryptedSessionName) ?: ''; if ($encryptedSessionData === '') { // Nothing to decrypt $this->sessionValues = []; } else { try { $this->sessionValues = json_decode( $this->crypto->decrypt($encryptedSessionData, $this->passphrase), true, 512, JSON_THROW_ON_ERROR, ); } catch (\Exception $e) { logger('core')->critical('Could not decrypt or decode encrypted session data', [ 'exception' => $e, ]); $this->sessionValues = []; $this->regenerateId(true, false); } } } /** * Set a value in the session * * @param string $key * @param mixed $value */ public function set(string $key, $value) { if ($this->get($key) === $value) { // Do not write the session if the value hasn't changed to avoid reopening return; } $reopened = $this->reopen(); $this->sessionValues[$key] = $value; $this->isModified = true; if ($reopened) { $this->close(); } } /** * Get a value from the session * * @param string $key * @return string|null Either the value or null */ public function get(string $key) { if (isset($this->sessionValues[$key])) { return $this->sessionValues[$key]; } return null; } /** * Check if a named key exists in the session * * @param string $key * @return bool */ public function exists(string $key): bool { return isset($this->sessionValues[$key]); } /** * Remove a $key/$value pair from the session * * @param string $key */ public function remove(string $key) { $reopened = $this->reopen(); $this->isModified = true; unset($this->sessionValues[$key]); if ($reopened) { $this->close(); } } /** * Reset and recreate the session */ public function clear() { $reopened = $this->reopen(); $requesttoken = $this->get('requesttoken'); $this->sessionValues = []; if ($requesttoken !== null) { $this->set('requesttoken', $requesttoken); } $this->isModified = true; $this->session->clear(); if ($reopened) { $this->close(); } } public function reopen(): bool { $reopened = $this->session->reopen(); if ($reopened) { $this->initializeSession(); } return $reopened; } /** * Wrapper around session_regenerate_id * * @param bool $deleteOldSession Whether to delete the old associated session file or not. * @param bool $updateToken Wheater to update the associated auth token * @return void */ public function regenerateId(bool $deleteOldSession = true, bool $updateToken = false) { $this->session->regenerateId($deleteOldSession, $updateToken); } /** * Wrapper around session_id * * @return string * @throws SessionNotAvailableException * @since 9.1.0 */ public function getId(): string { return $this->session->getId(); } /** * Close the session and release the lock, also writes all changed data in batch */ public function close() { if ($this->isModified) { $encryptedValue = $this->crypto->encrypt(json_encode($this->sessionValues), $this->passphrase); $this->session->set(self::encryptedSessionName, $encryptedValue); $this->isModified = false; } $this->session->close(); } /** * @param mixed $offset * @return bool */ public function offsetExists($offset): bool { return $this->exists($offset); } /** * @param mixed $offset * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); } /** * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value): void { $this->set($offset, $value); } /** * @param mixed $offset */ public function offsetUnset($offset): void { $this->remove($offset); } }