diff options
author | Bjoern Schiessle <schiessle@owncloud.com> | 2013-11-27 16:44:06 +0100 |
---|---|---|
committer | Bjoern Schiessle <schiessle@owncloud.com> | 2013-11-27 16:44:06 +0100 |
commit | 1065c3354326149022eaffe50c86c94ac226b620 (patch) | |
tree | 4a8f2ec09539d07bba84bfc73995fbbfcd220453 /apps/files_encryption | |
parent | 060e0ad0cd668529f13a97b866a371e505e8fa3e (diff) | |
parent | 40231c08cf617b5909dee778372d6b1b8cdd5bcf (diff) | |
download | nextcloud-server-1065c3354326149022eaffe50c86c94ac226b620.tar.gz nextcloud-server-1065c3354326149022eaffe50c86c94ac226b620.zip |
Merge branch 'master' into encryption_initial_enc_indicator
Diffstat (limited to 'apps/files_encryption')
-rwxr-xr-x | apps/files_encryption/lib/helper.php | 44 | ||||
-rwxr-xr-x | apps/files_encryption/lib/keymanager.php | 38 | ||||
-rw-r--r-- | apps/files_encryption/lib/proxy.php | 16 | ||||
-rw-r--r-- | apps/files_encryption/lib/stream.php | 27 | ||||
-rw-r--r-- | apps/files_encryption/lib/util.php | 86 | ||||
-rwxr-xr-x | apps/files_encryption/tests/crypt.php | 16 | ||||
-rw-r--r-- | apps/files_encryption/tests/keymanager.php | 4 |
7 files changed, 130 insertions, 101 deletions
diff --git a/apps/files_encryption/lib/helper.php b/apps/files_encryption/lib/helper.php index 0ac6fcf403a..17bcac5c585 100755 --- a/apps/files_encryption/lib/helper.php +++ b/apps/files_encryption/lib/helper.php @@ -225,10 +225,7 @@ class Helper { * @return bool */ public static function isPublicAccess() { - if (\OCP\USER::getUser() === false - || (isset($_GET['service']) && $_GET['service'] == 'files' - && isset($_GET['t'])) - ) { + if (\OCP\User::getUser() === false) { return true; } else { return false; @@ -256,10 +253,45 @@ class Helper { } /** - * @brief get path to the correspondig file in data/user/files if path points + * @brief try to get the user from the path if no user is logged in + * @param string $path + * @return mixed user or false if we couldn't determine a user + */ + public static function getUser($path) { + + $user = \OCP\User::getUser(); + + + // if we are logged in, then we return the userid + if ($user) { + return $user; + } + + // if no user is logged in we try to access a publicly shared files. + // In this case we need to try to get the user from the path + + $trimmed = ltrim($path, '/'); + $split = explode('/', $trimmed); + + // it is not a file relative to data/user/files + if (count($split) < 2 || $split[1] !== 'files') { + return false; + } + + $user = $split[0]; + + if (\OCP\User::userExists($user)) { + return $user; + } + + return false; + } + + /** + * @brief get path to the corresponding file in data/user/files if path points * to a version or to a file in cache * @param string $path path to a version or a file in the trash - * @return string path to correspondig file relative to data/user/files + * @return string path to corresponding file relative to data/user/files */ public static function getPathToRealFile($path) { $trimmed = ltrim($path, '/'); diff --git a/apps/files_encryption/lib/keymanager.php b/apps/files_encryption/lib/keymanager.php index 3427e8a963a..b2c756894b4 100755 --- a/apps/files_encryption/lib/keymanager.php +++ b/apps/files_encryption/lib/keymanager.php @@ -112,21 +112,18 @@ class Keymanager { * @brief store file encryption key * * @param \OC_FilesystemView $view + * @param \OCA\Encryption\Util $util * @param string $path relative path of the file, including filename - * @param $userId - * @param $catfile - * @internal param string $key + * @param string $catfile keyfile content * @return bool true/false * @note The keyfile is not encrypted here. Client code must * asymmetrically encrypt the keyfile before passing it to this method */ - public static function setFileKey(\OC_FilesystemView $view, $path, $userId, $catfile) { + public static function setFileKey(\OC_FilesystemView $view, $util, $path, $catfile) { $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - //here we need the currently logged in user, while userId can be a different user - $util = new Util($view, \OCP\User::getUser()); list($owner, $filename) = $util->getUidAndFilename($path); // in case of system wide mount points the keys are stored directly in the data directory @@ -172,15 +169,15 @@ class Keymanager { /** * @brief retrieve keyfile for an encrypted file * @param \OC_FilesystemView $view + * @param \OCA\Encryption\Util $util * @param $filePath * @internal param \OCA\Encryption\file $string name * @return string file key or false * @note The keyfile returned is asymmetrically encrypted. Decryption * of the keyfile must be performed by client code */ - public static function getFileKey(\OC_FilesystemView $view, $filePath) { + public static function getFileKey($view, $util, $filePath) { - $util = new Util($view, \OCP\User::getUser()); list($owner, $filename) = $util->getUidAndFilename($filePath); $filename = Helper::stripPartialFileExtension($filename); @@ -216,17 +213,17 @@ class Keymanager { * @brief Delete a keyfile * * @param \OC_FilesystemView $view - * @param string $userId username * @param string $path path of the file the key belongs to * @return bool Outcome of unlink operation * @note $path must be relative to data/user/files. e.g. mydoc.txt NOT * /data/admin/files/mydoc.txt */ - public static function deleteFileKey(\OC_FilesystemView $view, $userId, $path) { + public static function deleteFileKey(\OC_FilesystemView $view, $path) { $trimmed = ltrim($path, '/'); - $util = new Util($view, \OCP\User::getUser()); + $userId = Helper::getUser($path); + $util = new Util($view, $userId); if($util->isSystemWideMountPoint($path)) { $keyPath = '/files_encryption/keyfiles/' . $trimmed; @@ -315,16 +312,15 @@ class Keymanager { /** * @brief store multiple share keys for a single file * @param \OC_FilesystemView $view - * @param $path + * @param \OCA\Encryption\Util $util + * @param string $path * @param array $shareKeys * @return bool */ - public static function setShareKeys(\OC_FilesystemView $view, $path, array $shareKeys) { + public static function setShareKeys(\OC_FilesystemView $view, $util, $path, array $shareKeys) { // $shareKeys must be an array with the following format: // [userId] => [encrypted key] - // Here we need the currently logged in user, while userId can be a different user - $util = new Util($view, \OCP\User::getUser()); list($owner, $filename) = $util->getUidAndFilename($path); @@ -363,21 +359,18 @@ class Keymanager { * @brief retrieve shareKey for an encrypted file * @param \OC_FilesystemView $view * @param string $userId + * @param \OCA\Encryption\Util $util * @param string $filePath - * @internal param \OCA\Encryption\file $string name * @return string file key or false * @note The sharekey returned is encrypted. Decryption * of the keyfile must be performed by client code */ - public static function getShareKey(\OC_FilesystemView $view, $userId, $filePath) { + public static function getShareKey(\OC_FilesystemView $view, $userId, $util, $filePath) { // try reusing key file if part file $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - //here we need the currently logged in user, while userId can be a different user - $util = new Util($view, \OCP\User::getUser()); - list($owner, $filename) = $util->getUidAndFilename($filePath); $filename = Helper::stripPartialFileExtension($filename); // in case of system wide mount points the keys are stored directly in the data directory @@ -444,8 +437,9 @@ class Keymanager { $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; - //here we need the currently logged in user, while userId can be a different user - $util = new Util($view, \OCP\User::getUser()); + $userId = Helper::getUser($filePath); + + $util = new Util($view, $userId); list($owner, $filename) = $util->getUidAndFilename($filePath); diff --git a/apps/files_encryption/lib/proxy.php b/apps/files_encryption/lib/proxy.php index a8c74bd9dd4..5ba3bfa784f 100644 --- a/apps/files_encryption/lib/proxy.php +++ b/apps/files_encryption/lib/proxy.php @@ -47,8 +47,10 @@ class Proxy extends \OC_FileProxy { */ private static function shouldEncrypt($path) { + $userId = Helper::getUser($path); + if (\OCP\App::isEnabled('files_encryption') === false || Crypt::mode() !== 'server' || - strpos($path, '/' . \OCP\User::getUser() . '/files') !== 0) { + strpos($path, '/' . $userId . '/files') !== 0) { return false; } @@ -201,7 +203,7 @@ class Proxy extends \OC_FileProxy { list($owner, $ownerPath) = $util->getUidAndFilename($relativePath); // Delete keyfile & shareKey so it isn't orphaned - if (!Keymanager::deleteFileKey($view, $owner, $ownerPath)) { + if (!Keymanager::deleteFileKey($view, $ownerPath)) { \OCP\Util::writeLog('Encryption library', 'Keyfile or shareKey could not be deleted for file "' . $ownerPath . '"', \OCP\Util::ERROR); } @@ -244,9 +246,6 @@ class Proxy extends \OC_FileProxy { // split the path parts $pathParts = explode('/', $path); - // get relative path - $relativePath = \OCA\Encryption\Helper::stripUserFilesPath($path); - // FIXME: handling for /userId/cache used by webdav for chunking. The cache chunks are NOT encrypted if (isset($pathParts[2]) && $pathParts[2] === 'cache') { return $result; @@ -260,7 +259,8 @@ class Proxy extends \OC_FileProxy { $view = new \OC_FilesystemView(''); - $util = new Util($view, \OCP\USER::getUser()); + $userId = Helper::getUser($path); + $util = new Util($view, $userId); // If file is already encrypted, decrypt using crypto protocol if ( @@ -323,7 +323,7 @@ class Proxy extends \OC_FileProxy { $view = new \OC_FilesystemView('/'); - $userId = \OCP\User::getUser(); + $userId = Helper::getUser($path); $util = new Util($view, $userId); // if encryption is no longer enabled or if the files aren't migrated yet @@ -401,7 +401,7 @@ class Proxy extends \OC_FileProxy { $view = new \OC_FilesystemView('/'); $session = new \OCA\Encryption\Session($view); - $userId = \OCP\User::getUser(); + $userId = Helper::getUser($path); $util = new Util($view, $userId); // split the path parts diff --git a/apps/files_encryption/lib/stream.php b/apps/files_encryption/lib/stream.php index 1186a5f1d8d..7a37d2200a4 100644 --- a/apps/files_encryption/lib/stream.php +++ b/apps/files_encryption/lib/stream.php @@ -55,6 +55,7 @@ class Stream { private $rawPath; // The raw path relative to the data dir private $relPath; // rel path to users file dir private $userId; + private $keyId; private $handle; // Resource returned by fopen private $meta = array(); // Header / meta for source stream private $writeCache; @@ -90,17 +91,22 @@ class Stream { $this->rootView = new \OC_FilesystemView('/'); } - $this->session = new \OCA\Encryption\Session($this->rootView); - - $this->privateKey = $this->session->getPrivateKey($this->userId); - $util = new Util($this->rootView, \OCP\USER::getUser()); + $this->session = new \OCA\Encryption\Session($this->rootView); - $this->userId = $util->getUserId(); + $this->privateKey = $this->session->getPrivateKey(); // rawPath is relative to the data directory $this->rawPath = \OC\Files\Filesystem::normalizePath(str_replace('crypt://', '', $path)); + $this->userId = Helper::getUser($this->rawPath); + + $util = new Util($this->rootView, $this->userId); + + // get the key ID which we want to use, can be the users key or the + // public share key + $this->keyId = $util->getKeyId(); + // Strip identifier text from path, this gives us the path relative to data/<user>/files $this->relPath = Helper::stripUserFilesPath($this->rawPath); // if raw path doesn't point to a real file, check if it is a version or a file in the trash bin @@ -250,12 +256,13 @@ class Stream { // Fetch and decrypt keyfile // Fetch existing keyfile - $this->encKeyfile = Keymanager::getFileKey($this->rootView, $this->relPath); + $util = new \OCA\Encryption\Util($this->rootView, $this->userId); + $this->encKeyfile = Keymanager::getFileKey($this->rootView, $util, $this->relPath); // If a keyfile already exists if ($this->encKeyfile) { - $shareKey = Keymanager::getShareKey($this->rootView, $this->userId, $this->relPath); + $shareKey = Keymanager::getShareKey($this->rootView, $this->keyId, $util, $this->relPath); // if there is no valid private key return false if ($this->privateKey === false) { @@ -503,7 +510,7 @@ class Stream { \OC_FileProxy::$enabled = false; // Fetch user's public key - $this->publicKey = Keymanager::getPublicKey($this->rootView, $this->userId); + $this->publicKey = Keymanager::getPublicKey($this->rootView, $this->keyId); // Check if OC sharing api is enabled $sharingEnabled = \OCP\Share::isEnabled(); @@ -521,10 +528,10 @@ class Stream { $this->encKeyfiles = Crypt::multiKeyEncrypt($this->plainKey, $publicKeys); // Save the new encrypted file key - Keymanager::setFileKey($this->rootView, $this->relPath, $this->userId, $this->encKeyfiles['data']); + Keymanager::setFileKey($this->rootView, $util, $this->relPath, $this->encKeyfiles['data']); // Save the sharekeys - Keymanager::setShareKeys($this->rootView, $this->relPath, $this->encKeyfiles['keys']); + Keymanager::setShareKeys($this->rootView, $util, $this->relPath, $this->encKeyfiles['keys']); // Re-enable proxy - our work is done \OC_FileProxy::$enabled = $proxyStatus; diff --git a/apps/files_encryption/lib/util.php b/apps/files_encryption/lib/util.php index 7e46a5016a3..434ed225644 100644 --- a/apps/files_encryption/lib/util.php +++ b/apps/files_encryption/lib/util.php @@ -38,7 +38,8 @@ class Util { const MIGRATION_OPEN = 0; // user still needs to be migrated private $view; // OC_FilesystemView object for filesystem operations - private $userId; // ID of the currently logged-in user + private $userId; // ID of the user we use to encrypt/decrypt files + private $keyId; // ID of the key we want to manipulate private $client; // Client side encryption mode flag private $publicKeyDir; // Dir containing all public user keys private $encryptionDir; // Dir containing user's files_encryption @@ -58,51 +59,33 @@ class Util { public function __construct(\OC_FilesystemView $view, $userId, $client = false) { $this->view = $view; - $this->userId = $userId; $this->client = $client; - $this->isPublic = false; + $this->userId = $userId; $this->publicShareKeyId = \OC_Appconfig::getValue('files_encryption', 'publicShareKeyId'); $this->recoveryKeyId = \OC_Appconfig::getValue('files_encryption', 'recoveryKeyId'); - // if we are anonymous/public - if (\OCA\Encryption\Helper::isPublicAccess()) { - $this->userId = $this->publicShareKeyId; - - // only handle for files_sharing app - if (isset($GLOBALS['app']) && $GLOBALS['app'] === 'files_sharing') { - $this->userDir = '/' . $GLOBALS['fileOwner']; - $this->fileFolderName = 'files'; - $this->userFilesDir = '/' . $GLOBALS['fileOwner'] . '/' - . $this->fileFolderName; // TODO: Does this need to be user configurable? - $this->publicKeyDir = '/' . 'public-keys'; - $this->encryptionDir = '/' . $GLOBALS['fileOwner'] . '/' . 'files_encryption'; - $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; - $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; - $this->publicKeyPath = - $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key - $this->privateKeyPath = - '/owncloud_private_key/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key - $this->isPublic = true; - // make sure that the owners home is mounted - \OC\Files\Filesystem::initMountPoints($GLOBALS['fileOwner']); - } - - } else { - $this->userDir = '/' . $this->userId; - $this->fileFolderName = 'files'; - $this->userFilesDir = - '/' . $this->userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? - $this->publicKeyDir = '/' . 'public-keys'; - $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; - $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; - $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; - $this->publicKeyPath = + $this->userDir = '/' . $this->userId; + $this->fileFolderName = 'files'; + $this->userFilesDir = + '/' . $userId . '/' . $this->fileFolderName; // TODO: Does this need to be user configurable? + $this->publicKeyDir = '/' . 'public-keys'; + $this->encryptionDir = '/' . $this->userId . '/' . 'files_encryption'; + $this->keyfilesPath = $this->encryptionDir . '/' . 'keyfiles'; + $this->shareKeysPath = $this->encryptionDir . '/' . 'share-keys'; + $this->publicKeyPath = $this->publicKeyDir . '/' . $this->userId . '.public.key'; // e.g. data/public-keys/admin.public.key - $this->privateKeyPath = + $this->privateKeyPath = $this->encryptionDir . '/' . $this->userId . '.private.key'; // e.g. data/admin/admin.private.key - // make sure that the owners home is mounted - \OC\Files\Filesystem::initMountPoints($this->userId); + // make sure that the owners home is mounted + \OC\Files\Filesystem::initMountPoints($userId); + + if (\OCA\Encryption\Helper::isPublicAccess()) { + $this->keyId = $this->publicShareKeyId; + $this->isPublic = true; + } else { + $this->keyId = $this->userId; + $this->isPublic = false; } } @@ -188,13 +171,13 @@ class Util { // check if public-key exists but private-key is missing if ($this->view->file_exists($this->publicKeyPath) && !$this->view->file_exists($this->privateKeyPath)) { \OCP\Util::writeLog('Encryption library', - 'public key exists but private key is missing for "' . $this->userId . '"', \OCP\Util::FATAL); + 'public key exists but private key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL); return false; } else { if (!$this->view->file_exists($this->publicKeyPath) && $this->view->file_exists($this->privateKeyPath) ) { \OCP\Util::writeLog('Encryption library', - 'private key exists but public key is missing for "' . $this->userId . '"', \OCP\Util::FATAL); + 'private key exists but public key is missing for "' . $this->keyId . '"', \OCP\Util::FATAL); return false; } } @@ -367,7 +350,7 @@ class Util { // scanning every file like this // will eat server resources :( if ( - Keymanager::getFileKey($this->view, $relPath) + Keymanager::getFileKey($this->view, $this, $relPath) && $isEncryptedPath ) { @@ -478,7 +461,7 @@ class Util { $relPath = Helper::stripUserFilesPath($path); } - $fileKey = Keymanager::getFileKey($this->view, $relPath); + $fileKey = Keymanager::getFileKey($this->view, $this, $relPath); if ($fileKey === false) { return false; @@ -1056,10 +1039,10 @@ class Util { private function decryptKeyfile($filePath, $privateKey) { // Get the encrypted keyfile - $encKeyfile = Keymanager::getFileKey($this->view, $filePath); + $encKeyfile = Keymanager::getFileKey($this->view, $this, $filePath); // The file has a shareKey and must use it for decryption - $shareKey = Keymanager::getShareKey($this->view, $this->userId, $filePath); + $shareKey = Keymanager::getShareKey($this->view, $this->keyId, $this, $filePath); $plainKeyfile = Crypt::multiKeyDecrypt($encKeyfile, $shareKey, $privateKey); @@ -1110,8 +1093,8 @@ class Util { // Save the recrypted key to it's owner's keyfiles directory // Save new sharekeys to all necessary user directory if ( - !Keymanager::setFileKey($this->view, $filePath, $fileOwner, $multiEncKey['data']) - || !Keymanager::setShareKeys($this->view, $filePath, $multiEncKey['keys']) + !Keymanager::setFileKey($this->view, $this, $filePath, $multiEncKey['data']) + || !Keymanager::setShareKeys($this->view, $this, $filePath, $multiEncKey['keys']) ) { \OCP\Util::writeLog('Encryption library', @@ -1337,7 +1320,7 @@ class Util { // handle public access if ($this->isPublic) { $filename = $path; - $fileOwnerUid = $GLOBALS['fileOwner']; + $fileOwnerUid = $this->userId; return array( $fileOwnerUid, @@ -1565,6 +1548,13 @@ class Util { /** * @return string */ + public function getKeyId() { + return $this->keyId; + } + + /** + * @return string + */ public function getUserFilesDir() { return $this->userFilesDir; } diff --git a/apps/files_encryption/tests/crypt.php b/apps/files_encryption/tests/crypt.php index 9c32ee06453..ca14e3e2ccb 100755 --- a/apps/files_encryption/tests/crypt.php +++ b/apps/files_encryption/tests/crypt.php @@ -157,6 +157,8 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { $filename = 'tmp-' . time() . '.test'; + $util = new Encryption\Util(new \OC_FilesystemView(), $this->userId); + $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/'. $filename, $this->dataShort); // Test that data was successfully written @@ -176,10 +178,10 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { $this->assertNotEquals($this->dataShort, $retreivedCryptedFile); // Get the encrypted keyfile - $encKeyfile = Encryption\Keymanager::getFileKey($this->view, $filename); + $encKeyfile = Encryption\Keymanager::getFileKey($this->view, $util, $filename); // Attempt to fetch the user's shareKey - $shareKey = Encryption\Keymanager::getShareKey($this->view, $this->userId, $filename); + $shareKey = Encryption\Keymanager::getShareKey($this->view, $this->userId, $util, $filename); // get session $session = new \OCA\Encryption\Session($this->view); @@ -199,7 +201,7 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { // Teardown $this->view->unlink($this->userId . '/files/' . $filename); - Encryption\Keymanager::deleteFileKey($this->view, $this->userId, $filename); + Encryption\Keymanager::deleteFileKey($this->view, $filename); } /** @@ -214,6 +216,8 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { // Generate a a random filename $filename = 'tmp-' . time() . '.test'; + $util = new Encryption\Util(new \OC_FilesystemView(), $this->userId); + // Save long data as encrypted file using stream wrapper $cryptedFile = file_put_contents('crypt:///' . $this->userId . '/files/' . $filename, $this->dataLong . $this->dataLong); @@ -250,10 +254,10 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { //print_r($e); // Get the encrypted keyfile - $encKeyfile = Encryption\Keymanager::getFileKey($this->view, $filename); + $encKeyfile = Encryption\Keymanager::getFileKey($this->view, $util, $filename); // Attempt to fetch the user's shareKey - $shareKey = Encryption\Keymanager::getShareKey($this->view, $this->userId, $filename); + $shareKey = Encryption\Keymanager::getShareKey($this->view, $this->userId, $util, $filename); // get session $session = new \OCA\Encryption\Session($this->view); @@ -283,7 +287,7 @@ class Test_Encryption_Crypt extends \PHPUnit_Framework_TestCase { $this->view->unlink($this->userId . '/files/' . $filename); - Encryption\Keymanager::deleteFileKey($this->view, $this->userId, $filename); + Encryption\Keymanager::deleteFileKey($this->view, $filename); } diff --git a/apps/files_encryption/tests/keymanager.php b/apps/files_encryption/tests/keymanager.php index ad6bbd3a7e9..1467979f005 100644 --- a/apps/files_encryption/tests/keymanager.php +++ b/apps/files_encryption/tests/keymanager.php @@ -145,13 +145,15 @@ class Test_Encryption_Keymanager extends \PHPUnit_Framework_TestCase { $file = 'unittest-' . time() . '.txt'; + $util = new Encryption\Util($this->view, $this->userId); + // Disable encryption proxy to prevent recursive calls $proxyStatus = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $this->view->file_put_contents($this->userId . '/files/' . $file, $this->dataShort); - Encryption\Keymanager::setFileKey($this->view, $file, $this->userId, $key); + Encryption\Keymanager::setFileKey($this->view, $util, $file, $key); $this->assertTrue($this->view->file_exists('/' . $this->userId . '/files_encryption/keyfiles/' . $file . '.key')); |