From 8fdc52636f22865a77b1b1f4fcf2e41b7dcf28d6 Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Tue, 19 Oct 2021 13:48:50 +0100 Subject: [PATCH] [ActivityPub] Port RSA --- composer.json | 273 +++++++++--------- .../ActivityPub/Entity/ActivitypubActor.php | 20 +- plugins/ActivityPub/Entity/ActivitypubRsa.php | 199 +++++++++++++ .../Util/Model/EntityToType/GSActorToType.php | 21 +- src/Core/DB/DB.php | 2 +- 5 files changed, 363 insertions(+), 152 deletions(-) create mode 100644 plugins/ActivityPub/Entity/ActivitypubRsa.php diff --git a/composer.json b/composer.json index 77c7aa4a1f..7f87883ba5 100644 --- a/composer.json +++ b/composer.json @@ -1,139 +1,142 @@ { - "type": "project", - "name": "gnu/social", - "description": "Free software social networking platform.", - "license": "AGPL-3.0-only", - "require": { - "php": "^8.0", - "ext-ctype": "*", - "ext-curl": "*", - "ext-iconv": "*", - "ext-vips": "*", - "alchemy/zippy": "v0.5.x-dev", - "composer/package-versions-deprecated": "1.11.99.3", - "doctrine/annotations": "^1.0", - "doctrine/doctrine-bundle": "^2.4", - "doctrine/doctrine-migrations-bundle": "^3.1", - "doctrine/orm": "^2.9", - "erusev/parsedown": "^1.7", - "lstrojny/functional-php": "^1.11", - "nyholm/psr7": "^1.4", - "odolbeau/phone-number-bundle": "^3.1", - "oro/doctrine-extensions": "^2.0", - "php-ds/php-ds": "^1.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sensio/framework-extra-bundle": "^5.2", - "someonewithpc/memcached-polyfill": "^1.0", - "someonewithpc/redis-polyfill": "dev-master", - "symfony/asset": "5.2.*", - "symfony/cache": "5.2.*", - "symfony/config": "5.2.*", - "symfony/console": "5.2.*", - "symfony/dom-crawler": "5.2.*", - "symfony/dotenv": "5.2.*", - "symfony/event-dispatcher": "5.2.*", - "symfony/expression-language": "5.2.*", - "symfony/filesystem": "5.2.*", - "symfony/flex": "^1.3.1", - "symfony/form": "5.2.*", - "symfony/framework-bundle": "5.2.*", - "symfony/http-client": "5.2.*", - "symfony/intl": "5.2.*", - "symfony/mailer": "5.2.*", - "symfony/messenger": "5.2.*", - "symfony/mime": "5.2.*", - "symfony/monolog-bundle": "^3.1", - "symfony/notifier": "5.2.*", - "symfony/process": "5.2.*", - "symfony/property-access": "5.2.*", - "symfony/property-info": "5.2.*", - "symfony/proxy-manager-bridge": "5.2.*", - "symfony/security-bundle": "5.2.*", - "symfony/serializer": "5.2.*", - "symfony/string": "5.2.*", - "symfony/translation": "5.2.*", - "symfony/twig-bundle": "5.2.*", - "symfony/validator": "5.2.*", - "symfony/web-link": "5.2.*", - "symfony/yaml": "5.2.*", - "symfonycasts/reset-password-bundle": "^1.9", - "symfonycasts/verify-email-bundle": "^1.0", - "tgalopin/html-sanitizer-bundle": "^1.2", - "twig/extra-bundle": "^2.12|^3.0", - "twig/markdown-extra": "^3.0", - "twig/twig": "^2.12|^3.0", - "wikimedia/composer-merge-plugin": "^2.0" + "type": "project", + "name": "gnu/social", + "description": "Free software social networking platform.", + "license": "AGPL-3.0-only", + "require": { + "php": "^8.0", + "ext-ctype": "*", + "ext-curl": "*", + "ext-iconv": "*", + "ext-vips": "*", + "alchemy/zippy": "v0.5.x-dev", + "composer/package-versions-deprecated": "1.11.99.3", + "doctrine/annotations": "^1.0", + "doctrine/doctrine-bundle": "^2.4", + "doctrine/doctrine-migrations-bundle": "^3.1", + "doctrine/orm": "^2.9", + "erusev/parsedown": "^1.7", + "lstrojny/functional-php": "^1.11", + "nyholm/psr7": "^1.4", + "odolbeau/phone-number-bundle": "^3.1", + "oro/doctrine-extensions": "^2.0", + "php-ds/php-ds": "^1.2", + "phpdocumentor/reflection-docblock": "^5.2", + "sensio/framework-extra-bundle": "^5.2", + "someonewithpc/memcached-polyfill": "^1.0", + "someonewithpc/redis-polyfill": "dev-master", + "symfony/asset": "5.2.*", + "symfony/cache": "5.2.*", + "symfony/config": "5.2.*", + "symfony/console": "5.2.*", + "symfony/dom-crawler": "5.2.*", + "symfony/dotenv": "5.2.*", + "symfony/event-dispatcher": "5.2.*", + "symfony/expression-language": "5.2.*", + "symfony/filesystem": "5.2.*", + "symfony/flex": "^1.3.1", + "symfony/form": "5.2.*", + "symfony/framework-bundle": "5.2.*", + "symfony/http-client": "5.2.*", + "symfony/intl": "5.2.*", + "symfony/mailer": "5.2.*", + "symfony/messenger": "5.2.*", + "symfony/mime": "5.2.*", + "symfony/monolog-bundle": "^3.1", + "symfony/notifier": "5.2.*", + "symfony/process": "5.2.*", + "symfony/property-access": "5.2.*", + "symfony/property-info": "5.2.*", + "symfony/proxy-manager-bridge": "5.2.*", + "symfony/security-bundle": "5.2.*", + "symfony/serializer": "5.2.*", + "symfony/string": "5.2.*", + "symfony/translation": "5.2.*", + "symfony/twig-bundle": "5.2.*", + "symfony/validator": "5.2.*", + "symfony/web-link": "5.2.*", + "symfony/yaml": "5.2.*", + "symfonycasts/reset-password-bundle": "^1.9", + "symfonycasts/verify-email-bundle": "^1.0", + "tgalopin/html-sanitizer-bundle": "^1.2", + "twig/extra-bundle": "^2.12|^3.0", + "twig/markdown-extra": "^3.0", + "twig/twig": "^2.12|^3.0", + "wikimedia/composer-merge-plugin": "^2.0", + "ext-openssl": "*" + }, + "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^3.4", + "friendsofphp/php-cs-fixer": "^3.2.1", + "jchook/phpunit-assert-throws": "^1.0", + "niels-de-blaauw/php-doc-check": "^0.2.2", + "phpstan/phpstan": "^0.12.98", + "phpunit/phpunit": "^9.5", + "symfony/browser-kit": "^5.2", + "symfony/css-selector": "^5.2", + "symfony/debug-bundle": "^5.2", + "symfony/maker-bundle": "^1.14", + "symfony/phpunit-bridge": "^5.2", + "symfony/stopwatch": "^5.2", + "symfony/web-profiler-bundle": "^5.2", + "theofidry/psysh-bundle": "^4.4" + }, + "config": { + "preferred-install": { + "*": "dist" }, - "require-dev": { - "doctrine/doctrine-fixtures-bundle": "^3.4", - "friendsofphp/php-cs-fixer": "^3.2.1", - "jchook/phpunit-assert-throws": "^1.0", - "niels-de-blaauw/php-doc-check": "^0.2.2", - "phpstan/phpstan": "^0.12.98", - "phpunit/phpunit": "^9.5", - "symfony/browser-kit": "^5.2", - "symfony/css-selector": "^5.2", - "symfony/debug-bundle": "^5.2", - "symfony/maker-bundle": "^1.14", - "symfony/phpunit-bridge": "^5.2", - "symfony/stopwatch": "^5.2", - "symfony/web-profiler-bundle": "^5.2", - "theofidry/psysh-bundle": "^4.4" - }, - "config": { - "preferred-install": { - "*": "dist" - }, - "sort-packages": true - }, - "autoload": { - "files": ["src/Core/I18n/I18n.php"], - "psr-4": { - "App\\": "src/", - "Plugin\\": "plugins/", - "Component\\": "components/" - } - }, - "autoload-dev": { - "psr-4": { - "App\\Tests\\": "tests/" - } - }, - "replace": { - "paragonie/random_compat": "2.*", - "symfony/polyfill-ctype": "*", - "symfony/polyfill-iconv": "*", - "symfony/polyfill-php72": "*", - "symfony/polyfill-php71": "*", - "symfony/polyfill-php70": "*", - "symfony/polyfill-php56": "*" - }, - "scripts": { - "auto-scripts": { - "cache:clear": "symfony-cmd", - "assets:install %PUBLIC_DIR%": "symfony-cmd" - }, - "post-install-cmd": [ - "@auto-scripts", - "cp -fu bin/pre-commit .git/hooks" - ], - "post-update-cmd": [ - "@auto-scripts" - ] - }, - "conflict": { - "symfony/symfony": "*" - }, - "extra": { - "symfony": { - "allow-contrib": false, - "require": "5.2.*" - }, - "merge-plugin": { - "include": [ - "components/*/composer.json", - "plugins/*/composer.json" - ] - } + "sort-packages": true + }, + "autoload": { + "files": [ + "src/Core/I18n/I18n.php" + ], + "psr-4": { + "App\\": "src/", + "Plugin\\": "plugins/", + "Component\\": "components/" } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "paragonie/random_compat": "2.*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php56": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts", + "cp -fu bin/pre-commit .git/hooks" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "5.2.*" + }, + "merge-plugin": { + "include": [ + "components/*/composer.json", + "plugins/*/composer.json" + ] + } + } } diff --git a/plugins/ActivityPub/Entity/ActivitypubActor.php b/plugins/ActivityPub/Entity/ActivitypubActor.php index db9c18d26d..7aa241e38d 100644 --- a/plugins/ActivityPub/Entity/ActivitypubActor.php +++ b/plugins/ActivityPub/Entity/ActivitypubActor.php @@ -58,9 +58,10 @@ class ActivitypubActor extends Entity return $this->uri; } - public function setUri(string $uri): void + public function setUri(string $uri): self { $this->uri = $uri; + return $this; } public function getActorId(): int @@ -68,9 +69,10 @@ class ActivitypubActor extends Entity return $this->actor_id; } - public function setActorId(int $actor_id): void + public function setActorId(int $actor_id): self { $this->actor_id = $actor_id; + return $this; } public function getInboxUri(): string @@ -78,9 +80,10 @@ class ActivitypubActor extends Entity return $this->inbox_uri; } - public function setInboxUri(string $inbox_uri): void + public function setInboxUri(string $inbox_uri): self { $this->inbox_uri = $inbox_uri; + return $this; } public function getInboxSharedUri(): string @@ -88,9 +91,10 @@ class ActivitypubActor extends Entity return $this->inbox_shared_uri; } - public function setInboxSharedUri(string $inbox_shared_uri): void + public function setInboxSharedUri(string $inbox_shared_uri): self { $this->inbox_shared_uri = $inbox_shared_uri; + return $this; } public function getCreated(): DateTimeInterface @@ -98,9 +102,10 @@ class ActivitypubActor extends Entity return $this->created; } - public function setCreated(DateTimeInterface $created): void + public function setCreated(DateTimeInterface $created): self { $this->created = $created; + return $this; } public function getModified(): DateTimeInterface @@ -108,14 +113,15 @@ class ActivitypubActor extends Entity return $this->modified; } - public function setModified(DateTimeInterface $modified): void + public function setModified(DateTimeInterface $modified): self { $this->modified = $modified; + return $this; } // @codeCoverageIgnoreEnd // }}} Autocode - public static function schemaDef() + public static function schemaDef(): array { return [ 'name' => 'activitypub_actor', diff --git a/plugins/ActivityPub/Entity/ActivitypubRsa.php b/plugins/ActivityPub/Entity/ActivitypubRsa.php new file mode 100644 index 0000000000..e55f617214 --- /dev/null +++ b/plugins/ActivityPub/Entity/ActivitypubRsa.php @@ -0,0 +1,199 @@ +. + +namespace Plugin\ActivityPub\Entity; + +use App\Core\DB\DB; +use App\Core\Entity; +use App\Core\Log; +use App\Entity\Actor; +use App\Util\Exception\ServerException; +use DateTimeInterface; +use Exception; + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Peralta Cordeiro <@diogo.site> + * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +/** + * ActivityPub Keys System + * + * @category Plugin + * @package GNUsocial + * @author Diogo Peralta Cordeiro <@diogo.site> + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class ActivitypubRsa extends Entity +{ + // {{{ Autocode + // @codeCoverageIgnoreStart + private int $actor_id; + private ?string $private_key = null; + private string $public_key; + private DateTimeInterface $created; + private DateTimeInterface $modified; + + public function getActorId(): int + { + return $this->actor_id; + } + + public function setActorId(int $actor_id): self + { + $this->actor_id = $actor_id; + return $this; + } + + public function getPrivateKey(): string + { + return $this->private_key; + } + + public function setPrivateKey(string $private_key): self + { + $this->private_key = $private_key; + return $this; + } + + public function getPublicKey(): string + { + return $this->public_key; + } + + public function setPublicKey(string $public_key): self + { + $this->public_key = $public_key; + return $this; + } + + public function getCreated(): DateTimeInterface + { + return $this->created; + } + + public function setCreated(DateTimeInterface $created): self + { + $this->created = $created; + return $this; + } + + public function getModified(): DateTimeInterface + { + return $this->modified; + } + + public function setModified(DateTimeInterface $modified): self + { + $this->modified = $modified; + return $this; + } + // @codeCoverageIgnoreEnd + // }}} Autocode + + /** + * Return table definition for Schema setup and Entity usage. + * + * @return array array of column definitions + */ + public static function schemaDef(): array + { + return [ + 'name' => 'activitypub_rsa', + 'fields' => [ + 'actor_id' => ['type' => 'int', 'not null' => true], + 'private_key' => ['type' => 'text'], + 'public_key' => ['type' => 'text', 'not null' => true], + 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], + 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], + ], + 'primary key' => ['actor_id'], + 'foreign keys' => [ + 'activitypub_rsa_actor_id_fkey' => ['actor', ['actor_id' => 'id']], + ], + ]; + } + + /** + * Guarantees RSA keys for a given actor. + * + * @param Actor $gsactor + * @param bool $fetch=true Should attempt to fetch keys from a remote profile? + * @return ActivitypubRsa The keys (private key is null for remote actors) + * @throws ServerException It should never occur, but if so, we break everything! + */ + public static function getByActor(Actor $gsactor, bool $fetch = true): self + { + $apRSA = self::getWithPK(['actor_id' => ($actor_id = $gsactor->getId())]); + if (!$apRSA instanceof self) { + // Nonexistent key pair for this profile + try { + // throws exception if remote + $gsactor->getLocalUser(); + + self::generateKeys($private_key, $public_key); + + $apRSA = new self(); + $apRSA->setActorId($actor_id); + $apRSA->setPrivateKey($private_key); + $apRSA->setPublicKey($public_key); + } catch (Exception) { + // ASSERT: This should never happen, but try to recover! + Log::error("Activitypub_rsa: An impossible thing has happened... Please let the devs know."); + if ($fetch) { + //$res = Activitypub_explorer::get_remote_user_activity($profile->getUri()); + //Activitypub_rsa::update_public_key($profile, $res['publicKey']['publicKeyPem']); + //return self::ensure_public_key($profile, false); + } else { + throw new ServerException('Activitypub_rsa: Failed to find keys for given profile. That should have not happened!'); + } + } + } + return $apRSA; + } + + /** + * Generates a pair of RSA keys. + * + * @param string|null $private_key out + * @param string|null $public_key out + * @author PHP Manual Contributed Notes + */ + private static function generateKeys(?string &$private_key, ?string &$public_key): void + { + $config = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]; + + // Create the private and public key + $res = openssl_pkey_new($config); + + // Extract the private key from $res to $private_key + openssl_pkey_export($res, $private_key); + + // Extract the public key from $res to $pubKey + $pubKey = openssl_pkey_get_details($res); + $public_key = $pubKey["key"]; + unset($pubKey); + } +} diff --git a/plugins/ActivityPub/Util/Model/EntityToType/GSActorToType.php b/plugins/ActivityPub/Util/Model/EntityToType/GSActorToType.php index f0c4d62c4e..4ec7306ff9 100644 --- a/plugins/ActivityPub/Util/Model/EntityToType/GSActorToType.php +++ b/plugins/ActivityPub/Util/Model/EntityToType/GSActorToType.php @@ -11,20 +11,23 @@ use Component\Avatar\Avatar; use Component\Avatar\Exception\NoAvatarException; use DateTimeInterface; use Exception; +use Plugin\ActivityPub\Entity\ActivitypubRsa; use Plugin\ActivityPub\Util\Type; +use Plugin\ActivityPub\Util\Type\Extended\Actor\Person; class GSActorToType { /** *@throws Exception */ - public static function translate(Actor $gsactor): Type + public static function translate(Actor $gsactor): Person { + $rsa = ActivitypubRsa::getByActor($gsactor); + $public_key = $rsa->getPublicKey(); $uri = null; - Event::handle('FreeNetworkGenerateLocalActorUri', ['source' => 'ActivityPub', 'actor_id' => $gsactor->getId(), 'actor_uri' => &$attributedTo]); $attr = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $uri, + 'id' => $gsactor->getUri(Router::ABSOLUTE_URL), 'inbox' => Router::url('activitypub_actor_inbox', ['gsactor_id' => $gsactor->getId()], Router::ABSOLUTE_URL), 'outbox' => Router::url('activitypub_actor_outbox', ['gsactor_id' => $gsactor->getId()], Router::ABSOLUTE_URL), 'following' => Router::url('actor_subscriptions_id', ['id' => $gsactor->getId()], Router::ABSOLUTE_URL), @@ -32,18 +35,18 @@ class GSActorToType 'liked' => Router::url('actor_favourites_id', ['id' => $gsactor->getId()], Router::ABSOLUTE_URL), //'streams' => 'preferredUsername' => $gsactor->getNickname(), - //'publicKey' => [ - // 'id' => $uri . "#public-key", - // 'owner' => $uri, - // 'publicKeyPem' => $public_key - // ], + 'publicKey' => [ + 'id' => $uri . "#public-key", + 'owner' => $uri, + 'publicKeyPem' => $public_key + ], 'name' => $gsactor->getFullname(), 'location' => $gsactor->getLocation(), 'published' => $gsactor->getCreated()->format(DateTimeInterface::RFC3339), 'summary' => $gsactor->getBio(), //'tag' => $gsactor->getSelfTags(), 'updated' => $gsactor->getModified()->format(DateTimeInterface::RFC3339), - 'url' => Router::url('actor_view_nickname', ['nickname' => $gsactor->getNickname()], Router::ABSOLUTE_URL), + 'url' => $gsactor->getUrl(Router::ABSOLUTE_URL), ]; try { $attr['icon'] = Avatar::getAvatar($gsactor->getId())->getUrl(type: Router::ABSOLUTE_URL); diff --git a/src/Core/DB/DB.php b/src/Core/DB/DB.php index 016f07117c..b5b5ccb3e5 100644 --- a/src/Core/DB/DB.php +++ b/src/Core/DB/DB.php @@ -68,7 +68,7 @@ class DB { $all = self::$em->getMetadataFactory()->getAllMetadata(); foreach ($all as $meta) { - self::$table_map[$meta->getTableName()] = $meta->getMetadataValue('name'); + self::$table_map[$meta->getTableName()] = $meta->getMetadataValue('name'); self::$class_pk[$meta->getMetadataValue('name')] = $meta->getIdentifier(); } }