port hubzillas OpenWebAuth - remote authentification
authorrabuzarus <rabuzarus@t-online.de>
Mon, 18 Jun 2018 21:05:44 +0000 (23:05 +0200)
committerrabuzarus <rabuzarus@t-online.de>
Mon, 18 Jun 2018 21:05:44 +0000 (23:05 +0200)
16 files changed:
boot.php
database.sql
doc/Addons.md
index.php
mod/xrd.php
src/Core/System.php
src/Database/DBStructure.php
src/Model/Profile.php
src/Model/Verify.php [new file with mode: 0644]
src/Module/Magic.php [new file with mode: 0644]
src/Module/Owa.php [new file with mode: 0644]
src/Network/Probe.php
src/Util/Crypto.php
src/Util/HTTPHeaders.php [new file with mode: 0644]
src/Util/HTTPSig.php [new file with mode: 0644]
view/templates/xrd_person.tpl

index 79ec53a..46bb2a3 100644 (file)
--- a/boot.php
+++ b/boot.php
@@ -41,7 +41,7 @@ define('FRIENDICA_PLATFORM',     'Friendica');
 define('FRIENDICA_CODENAME',     'The Tazmans Flax-lily');
 define('FRIENDICA_VERSION',      '2018.08-dev');
 define('DFRN_PROTOCOL_VERSION',  '2.23');
-define('DB_UPDATE_VERSION',      1268);
+define('DB_UPDATE_VERSION',      1269);
 define('NEW_UPDATE_ROUTINE_VERSION', 1170);
 
 /**
index b186c0c..b871ce2 100644 (file)
@@ -1,6 +1,6 @@
 -- ------------------------------------------
 -- Friendica 2018.08-dev (The Tazmans Flax-lily)
--- DB_UPDATE_VERSION 1268
+-- DB_UPDATE_VERSION 1269
 -- ------------------------------------------
 
 
@@ -375,7 +375,7 @@ CREATE TABLE IF NOT EXISTS `group` (
 CREATE TABLE IF NOT EXISTS `group_member` (
        `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID',
        `gid` int unsigned NOT NULL DEFAULT 0 COMMENT 'groups.id of the associated group',
-       `contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id  of the member assigned to the associated group',
+       `contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id of the member assigned to the associated group',
         PRIMARY KEY(`id`),
         INDEX `contactid` (`contact-id`),
         UNIQUE INDEX `gid_contactid` (`gid`,`contact-id`)
@@ -1084,6 +1084,19 @@ CREATE TABLE IF NOT EXISTS `user-item` (
         PRIMARY KEY(`uid`,`iid`)
 ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific item data';
 
+--
+-- TABLE verify
+--
+CREATE TABLE IF NOT EXISTS `verify` (
+       `id` int(10) NOT NULL auto_increment COMMENT 'sequential ID',
+       `uid` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'User id',
+       `type` varchar(32) DEFAULT '' COMMENT 'Verify type',
+       `token` varchar(255) DEFAULT '' COMMENT 'A generated token',
+       `meta` varchar(255) DEFAULT '' COMMENT '',
+       `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime of creation',
+        PRIMARY KEY(`id`)
+) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Store token to verify contacts';
+
 --
 -- TABLE worker-ipc
 --
index 22b34fa..090d5a9 100644 (file)
@@ -357,6 +357,13 @@ Hook data:
     'item' => item array (input)
     'html' => converted item body (input/output)
 
+### 'magic_auth_success'
+Called when a magic-auth was successful.
+Hook data:
+    'visitor' => array with the contact record of the visitor
+    'url' => the query string
+    'session' => $_SESSION array
+
 Current JavaScript hooks
 -------------
 
@@ -557,6 +564,7 @@ Here is a complete list of all hook callbacks with file locations (as of 01-Apr-
     Addon::callHooks('profile_sidebar', $arr);
     Addon::callHooks('profile_tabs', $arr);
     Addon::callHooks('zrl_init', $arr);
+    Addon::callHooks('magic_auth_success', $arr);
 
 ### src/Model/Event.php
 
index aeda999..c0290df 100644 (file)
--- a/index.php
+++ b/index.php
@@ -121,25 +121,35 @@ if ((x($_SESSION, 'language')) && ($_SESSION['language'] !== $lang)) {
        L10n::loadTranslationTable($lang);
 }
 
-if ((x($_GET, 'zrl')) && $a->mode == App::MODE_NORMAL) {
-       // Only continue when the given profile link seems valid
-       // Valid profile links contain a path with "/profile/" and no query parameters
-       if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "")
-               && strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")
-       ) {
-               $_SESSION['my_url'] = $_GET['zrl'];
-               $a->query_string = preg_replace('/[\?&]zrl=(.*?)([\?&]|$)/is', '', $a->query_string);
-               Profile::zrlInit($a);
-       } else {
-               // Someone came with an invalid parameter, maybe as a DDoS attempt
-               // We simply stop processing here
-               logger("Invalid ZRL parameter ".$_GET['zrl'], LOGGER_DEBUG);
-               header('HTTP/1.1 403 Forbidden');
-               echo "<h1>403 Forbidden</h1>";
-               killme();
+if ((x($_GET,'zrl')) && $a->mode == App::MODE_NORMAL) {
+       $a->query_string = Profile::stripZrls($a->query_string);
+       if (!local_user()) {
+               // Only continue when the given profile link seems valid
+               // Valid profile links contain a path with "/profile/" and no query parameters
+               if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
+                       strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
+                       if ($_SESSION["visitor_home"] != $_GET["zrl"]) {
+                               $_SESSION['my_url'] = $_GET['zrl'];
+                               $_SESSION['authenticated'] = 0;
+                       }
+                       Profile::zrlInit($a);
+               } else {
+                       // Someone came with an invalid parameter, maybe as a DDoS attempt
+                       // We simply stop processing here
+                       logger("Invalid ZRL parameter " . $_GET['zrl'], LOGGER_DEBUG);
+                       header('HTTP/1.1 403 Forbidden');
+                       echo "<h1>403 Forbidden</h1>";
+                       killme();
+               }
        }
 }
 
+if ((x($_GET,'owt')) && $a->mode == App::MODE_NORMAL) {
+       $token = $_GET['owt'];
+       $a->query_string = Profile::stripQueryParam($a->query_string, 'owt');
+       Profile::owtInit($token);
+}
+
 /**
  * For Mozilla auth manager - still needs sorting, and this might conflict with LRDD header.
  * Apache/PHP lumps the Link: headers into one - and other services might not be able to parse it
index bbfd7ce..2d19bb3 100644 (file)
@@ -78,7 +78,8 @@ function xrd_json($a, $uri, $alias, $profile_url, $r)
                                        ['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
                                        ['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => System::baseUrl().'/salmon/'.$r['nickname'].'/mention'],
                                        ['rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => System::baseUrl().'/follow?url={uri}'],
-                                       ['rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,'.$salmon_key]
+                                       ['rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,'.$salmon_key],
+                                       array('rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-dfrn+json', 'href' => System::baseUrl().'/owa')
        ]];
        echo json_encode($json);
        killme();
@@ -102,10 +103,11 @@ function xrd_xml($a, $uri, $alias, $profile_url, $r)
                '$atom'        => System::baseUrl() . '/dfrn_poll/'     . $r['nickname'],
                '$poco_url'    => System::baseUrl() . '/poco/'          . $r['nickname'],
                '$photo'       => System::baseUrl() . '/photo/profile/' . $r['uid']      . '.jpg',
-               '$baseurl' => System::baseUrl(),
+               '$baseurl'     => System::baseUrl(),
                '$salmon'      => System::baseUrl() . '/salmon/'        . $r['nickname'],
                '$salmen'      => System::baseUrl() . '/salmon/'        . $r['nickname'] . '/mention',
                '$subscribe'   => System::baseUrl() . '/follow?url={uri}',
+               '$openwebauth' => System::baseUrl() .'/owa',
                '$modexp'      => 'data:application/magic-public-key,'  . $salmon_key]
        );
 
index 1db417e..ded781d 100644 (file)
@@ -163,17 +163,17 @@ EOT;
        }
 
        /**
-        * @brief Encodes content to json
+        * @brief Encodes content to json.
         *
         * This function encodes an array to json format
         * and adds an application/json HTTP header to the output.
         * After finishing the process is getting killed.
         *
-        * @param array $x The input content
+        * @param array  $x The input content.
+        * @param string $content_type Type of the input (Default: 'application/json').
         */
-       public static function jsonExit($x)
-       {
-               header("content-type: application/json");
+       public static function jsonExit($x, $content_type = 'application/json') {
+               header("Content-type: $content_type");
                echo json_encode($x);
                killme();
        }
index d441955..33babde 100644 (file)
@@ -1818,6 +1818,20 @@ class DBStructure
                                                "PRIMARY" => ["uid", "iid"],
                                                ]
                                ];
+               $database["verify"] = [
+                               "comment" => "Store token to verify contacts",
+                               "fields" => [
+                                               "id" => ["type" => "int(10)", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"],
+                                               "uid" => ["type" => "int(10) unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "User id"],
+                                               "type" => ["type" => "varchar(32)", "not_null", "default" => "", "comment" => "Verify type"],
+                                               "token" => ["type" => "varchar(255)", "not_null" => "1", "default" => "", "comment" => "A generated token"],
+                                               "meta" => ["type" => "varchar(255)", "not_null" => "1", "default" => "", "comment" => ""],
+                                               "created" => ["type" => "datetime", "not null" => "1", "default" => NULL_DATE, "comment" => "datetime of creation"],
+                                       ],
+                               "indexes" => [
+                                               "PRIMARY" => ["id"],
+                                               ]
+                               ];
                $database["worker-ipc"] = [
                                "comment" => "Inter process communication between the frontend and the worker",
                                "fields" => [
index 39a8969..cb1a15a 100644 (file)
@@ -17,7 +17,9 @@ use Friendica\Core\System;
 use Friendica\Core\Worker;
 use Friendica\Database\DBM;
 use Friendica\Model\Contact;
+use Friendica\Model\Verify;
 use Friendica\Protocol\Diaspora;
+use Friendica\Network\Probe;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Util\Network;
 use Friendica\Util\Temporal;
@@ -978,25 +980,135 @@ class Profile
                return null;
        }
 
+       /**
+        * Process the 'zrl' parameter and initiate the remote authentication.
+        * 
+        * This method checks if the visitor has a public contact entry and
+        * redirects the visitor to his/her instance to start the magic auth (Authentication)
+        * process.
+        * 
+        * @param App $a Application instance.
+        */
        public static function zrlInit(App $a)
        {
                $my_url = self::getMyURL();
                $my_url = Network::isUrlValid($my_url);
+
                if ($my_url) {
-                       // Is it a DDoS attempt?
-                       // The check fetches the cached value from gprobe to reduce the load for this system
-                       $urlparts = parse_url($my_url);
+                       if (!local_user()) {
+                               // Is it a DDoS attempt?
+                               // The check fetches the cached value from gprobe to reduce the load for this system
+                               $urlparts = parse_url($my_url);
+
+                               $result = Cache::get('gprobe:' . $urlparts['host']);
+                               if ((!is_null($result)) && (in_array($result['network'], [NETWORK_FEED, NETWORK_PHANTOM]))) {
+                                       logger('DDoS attempt detected for ' . $urlparts['host'] . ' by ' . $_SERVER['REMOTE_ADDR'] . '. server data: ' . print_r($_SERVER, true), LOGGER_DEBUG);
+                                       return;
+                               }
 
-                       $result = Cache::get('gprobe:' . $urlparts['host']);
-                       if ((!is_null($result)) && (in_array($result['network'], [NETWORK_FEED, NETWORK_PHANTOM]))) {
-                               logger('DDoS attempt detected for ' . $urlparts['host'] . ' by ' . $_SERVER['REMOTE_ADDR'] . '. server data: ' . print_r($_SERVER, true), LOGGER_DEBUG);
-                               return;
+                               Worker::add(PRIORITY_LOW, 'GProbe', $my_url);
+                               $arr = ['zrl' => $my_url, 'url' => $a->cmd];
+                               Addon::callHooks('zrl_init', $arr);
+
+                               // Try to find the public contact entry of the visitor.
+                               $fields = ["id", "url"];
+                               $condition = ['uid' => 0, 'nurl' => normalise_link($my_url)];
+
+                               $contact = dba::selectFirst('contact',$fields, $condition);
+
+                               // Not found? Try to probe the visitor.
+                               if (!DBM::is_result($contact)) {
+                                       Probe::uri($my_url, '', -1, true, true);
+                                       $contact = dba::selectFirst('contact',$fields, $condition);
+                               }
+
+                               if (!DBM::is_result($contact)) {
+                                       logger('No contact record found for ' . $my_url, LOGGER_DEBUG);
+                                       return;
+                               }
+
+                               if (DBM::is_result($contact) && remote_user() && remote_user() === $contact['id']) {
+                                       // The visitor is already authenticated.
+                                       return;
+                               }
+
+                               logger('Not authenticated. Invoking reverse magic-auth for ' . $my_url, LOGGER_DEBUG);
+
+                               // Try to avoid recursion - but send them home to do a proper magic auth.
+                               $query = str_replace(array('?zrl=', '&zid='), array('?rzrl=', '&rzrl='), $a->query_string);
+                               // The other instance needs to know where to redirect.
+                               $dest = urlencode(System::baseUrl() . "/" . $query);
+
+                               // We need to extract the basebath from the profile url
+                               // to redirect the visitors '/magic' module.
+                               // Note: We should have the basepath of a contact also in the contact table.
+                               $urlarr = explode("/profile/", $contact['url']);
+                               $basepath = $urlarr[0];
+
+                               if ($basepath != System::baseUrl() && !strstr($dest, '/magic') && !strstr($dest, '/rmagic')) {
+                                       goaway($basepath . '/magic' . '?f=&owa=1&dest=' . $dest);
+                               }
                        }
+               }
+       }
 
-                       Worker::add(PRIORITY_LOW, 'GProbe', $my_url);
-                       $arr = ['zrl' => $my_url, 'url' => $a->cmd];
-                       Addon::callHooks('zrl_init', $arr);
+       /**
+        * OpenWebAuth authentication.
+        *
+        * @param string $token
+        */
+       public static function owtInit($token)
+       {
+               $a = get_app();
+
+               // Clean old verify entries.
+               Verify::purge('owt', '3 MINUTE');
+
+               // Check if the token we got is the same one
+               // we have stored in the database.
+               $visitor_handle = Verify::getMeta('owt', 0, $token);
+
+               if($visitor_handle === false) {
+                       return;
+               }
+
+               // Try to find the public contact entry of the visitor.
+               $condition = ["uid" => 0, "addr" => $visitor_handle];
+               $visitor = dba::selectFirst("contact", [], $condition);
+
+               if (!DBM::is_result($visitor)) {
+                       Probe::uri($visitor_handle, '', -1, true, true);
+                       $visitor = dba::selectFirst("contact", [], $condition);
+               }
+               if(!DBM::is_result($visitor)) {
+                       logger('owt: unable to finger ' . $visitor_handle, LOGGER_DEBUG);
+                       return;
                }
+
+               // Authenticate the visitor.
+               $_SESSION['authenticated'] = 1;
+               $_SESSION['visitor_id'] = $visitor['id'];
+               $_SESSION['visitor_handle'] = $visitor['addr'];
+               $_SESSION['visitor_home'] = $visitor['url'];
+
+               $arr = [
+                       'visitor' => $visitor,
+                       'url' => $a->query_string,
+                       'session' => $_SESSION
+               ];
+               /**
+                * @hooks magic_auth_success
+                *   Called when a magic-auth was successful.
+                *   * \e array \b visitor
+                *   * \e string \b url
+                *   * \e array \b session
+                */
+               Addon::callHooks('magic_auth_success', $arr);
+               $a->contact = $visitor;
+
+               info(L10n::t('OpenWebAuth: %1$s welcomes %2$s', $a->get_hostname(), $visitor['name']));
+
+               logger('OpenWebAuth: auth success from ' . $visitor['addr'], LOGGER_DEBUG);
        }
 
        public static function zrl($s, $force = false)
@@ -1042,4 +1154,26 @@ class Profile
 
                return $uid;
        }
+
+       /**
+       * Stip zrl parameter from a string.
+       * 
+       * @param string $s The input string.
+       * @return string The zrl.
+       */
+       public static function stripZrls($s)
+       {
+               return preg_replace('/[\?&]zrl=(.*?)([\?&]|$)/is', '', $s);
+       }
+
+       /**
+       * Stip query parameter from a string.
+       * 
+       * @param string $s The input string.
+       * @return string The query parameter.
+       */
+       public static function stripQueryParam($s, $param)
+       {
+               return preg_replace('/[\?&]' . $param . '=(.*?)(&|$)/ism', '$2', $s);
+       }
 }
diff --git a/src/Model/Verify.php b/src/Model/Verify.php
new file mode 100644 (file)
index 0000000..92dbafd
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file src/Model/Verify.php
+ */
+namespace Friendica\Model;
+
+use Friendica\Database\DBM;
+use Friendica\Util\DateTimeFormat;
+use dba;
+
+/**
+ * Methods to deal with entries of the 'verify' table.
+ */
+class Verify
+{
+       /**
+        * Create an entry in the 'verify' table.
+        * 
+        * @param string $type   Verify type.
+        * @param int    $uid    The user ID.
+        * @param string $token
+        * @param string $meta
+        * 
+        * @return boolean
+        */
+       public static function create($type, $uid, $token, $meta)
+       {
+               $fields = [
+                       "type" => $type,
+                       "uid" => $uid,
+                       "token" => $token,
+                       "meta" => $meta,
+                       "created" => DateTimeFormat::utcNow()
+               ];
+               return dba::insert("verify", $fields);
+       }
+
+       /**
+        * Get the "meta" field of an entry in the verify table.
+        * 
+        * @param string $type   Verify type.
+        * @param int    $uid    The user ID.
+        * @param string $token
+        * 
+        * @return string|boolean The meta enry or false if not found.
+        */
+       public static function getMeta($type, $uid, $token)
+       {
+               $condition = ["type" => $type, "uid" => $uid, "token" => $token];
+
+               $entry = dba::selectFirst("verify", ["id", "meta"], $condition);
+               if (DBM::is_result($entry)) {
+                       dba::delete("verify", ["id" => $entry["id"]]);
+
+                       return $entry["meta"];
+               }
+               return false;
+       }
+
+       /**
+        * Purge entries of a verify-type older than interval.
+        * 
+        * @param string $type     Verify type.
+        * @param string $interval SQL compatible time interval
+        */
+       public static function purge($type, $interval)
+       {
+               $condition = ["`type` = ? AND `created` < ?", $type, DateTimeFormat::utcNow() . " - INTERVAL " . $interval];
+               dba::delete("verify", $condition);
+       }
+
+}
diff --git a/src/Module/Magic.php b/src/Module/Magic.php
new file mode 100644 (file)
index 0000000..fef970d
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * @file src/Module/Magic.php
+ */
+namespace Friendica\Module;
+
+use Friendica\BaseModule;
+use Friendica\Database\DBM;
+use Friendica\Network\Probe;
+use Friendica\Util\HTTPSig;
+use Friendica\Util\Network;
+
+use dba;
+
+class Magic extends BaseModule
+{
+       public static function init()
+       {
+               $a = self::getApp();
+               $ret = ['success' => false, 'url' => '', 'message' => ''];
+               logger('magic mdule: invoked', LOGGER_DEBUG);
+
+               logger('args: ' . print_r($_REQUEST, true), LOGGER_DATA);
+
+               $addr = ((x($_REQUEST, 'addr')) ? $_REQUEST['addr'] : '');
+               $dest = ((x($_REQUEST, 'dest')) ? $_REQUEST['dest'] : '');
+               $test = ((x($_REQUEST, 'test')) ? intval($_REQUEST['test']) : 0);
+               $owa  = ((x($_REQUEST, 'owa'))  ? intval($_REQUEST['owa'])  : 0);
+
+               // NOTE: I guess $dest isn't just the profile url (could be also 
+               // other profile pages e.g. photo). We need to find a solution
+               // to be able to redirct to other pages than the contact profile.
+               $fields = ["id", "nurl", "url"];
+               $condition = ["nurl" => normalise_link($dest)];
+
+               $contact = dba::selectFirst("contact", $fields, $condition);
+
+               if (!DBM::is_result($contact)) {
+                       // If we don't have a contact record, try to probe it.
+                       /// @todo: Also check against the $addr.
+                       Probe::uri($dest, '', -1, true, true);
+                       $contact = dba::selectFirst("contact", $fields, $condition);
+               }
+
+               if (!DBM::is_result($contact)) {
+                       logger("No contact record found: " . print_r($_REQUEST, true), LOGGER_DEBUG);
+                       goaway($dest);
+               }
+
+               // Redirect if the contact is already authenticated on this site.
+               if (array_key_exists("id", $a->contact) && strpos($contact['nurl'], normalise_link(self::getApp()->get_baseurl())) !== false) {
+                       if($test) {
+                               $ret['success'] = true;
+                               $ret['message'] .= 'Local site - you are already authenticated.' . EOL;
+                               return $ret;
+                       }
+
+                       logger("Contact is already authenticated", LOGGER_DEBUG);
+                       goaway($dest);
+               }
+
+               if (local_user()) {
+                       $user = $a->user;
+
+                       // OpenWebAuth
+                       if ($owa) {
+                               // Extract the basepath
+                               // NOTE: we need another solution because this does only work
+                               // for friendica contacts :-/ . We should have the basepath
+                               // of a contact also in the contact table.
+                               $exp = explode("/profile/", $contact['url']);
+                               $basepath = $exp[0];
+
+                               $headers = [];
+                               $headers['Accept'] = 'application/x-dfrn+json';
+                               $headers['X-Open-Web-Auth'] = random_string();
+
+                               // Create a header that is signed with the local users private key.
+                               $headers = HTTPSig::createSig(
+                                                       '',
+                                                       $headers,
+                                                       $user['prvkey'],
+                                                       'acct:' . $user['nickname'] . '@' . $a->get_hostname() . ($a->path ? '/' . $a->path : ''),
+                                                       false,
+                                                       true,
+                                                       'sha512'
+                               );
+
+                               // Try to get an authentication token from the other instance.
+                               $x = Network::curl($basepath . '/owa', false, $redirects, ['headers' => $headers]);
+
+                               if ($x['success']) {
+                                       $j = json_decode($x['body'], true);
+
+                                       if ($j['success']) {
+                                               $token = '';
+                                               if ($j['encrypted_token']) {
+                                                       // The token is encrypted. If the local user is really the one the other instance
+                                                       // thinks he/she is, the token can be decrypted with the local users public key.
+                                                       openssl_private_decrypt(base64url_decode($j['encrypted_token']), $token, $user['prvkey']);
+                                               } else {
+                                                       $token = $j['token'];
+                                               }
+                                               $x = strpbrk($dest, '?&');
+                                               $args = (($x) ? '&owt=' . $token : '?f=&owt=' . $token);
+
+                                               goaway($dest . $args);
+                                       }
+                               }
+                               goaway($dest);
+                       }
+               }
+
+               if($test) {
+                       $ret['message'] = 'Not authenticated or invalid arguments' . EOL;
+                       return $ret;
+               }
+
+               goaway($dest);
+       }
+}
diff --git a/src/Module/Owa.php b/src/Module/Owa.php
new file mode 100644 (file)
index 0000000..27c863e
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+/**
+ * @file src/Module/Owa.php
+ */
+namespace Friendica\Module;
+
+use Friendica\BaseModule;
+use Friendica\Core\System;
+use Friendica\Database\DBM;
+use Friendica\Model\Verify;
+use Friendica\Network\Probe;
+use Friendica\Util\DateTimeFormat;
+use Friendica\Util\HTTPSig;
+
+use dba;
+
+/**
+ * @brief OpenWebAuth verifier and token generator
+ * 
+ * See https://macgirvin.com/wiki/mike/OpenWebAuth/Home
+ * Requests to this endpoint should be signed using HTTP Signatures
+ * using the 'Authorization: Signature' authentication method
+ * If the signature verifies a token is returned.
+ *
+ * This token may be exchanged for an authenticated cookie.
+ */
+class Owa extends BaseModule
+{
+       public static function init()
+       {
+
+               $ret = [ 'success' => false ];
+
+               foreach (['REDIRECT_REMOTE_USER', 'HTTP_AUTHORIZATION'] as $head) {
+                       if (array_key_exists($head, $_SERVER) && substr(trim($_SERVER[$head]), 0, 9) === 'Signature') {
+                               if ($head !== 'HTTP_AUTHORIZATION') {
+                                       $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER[$head];
+                                       continue;
+                               }
+
+                               $sigblock = HTTPSig::parseSigheader($_SERVER[$head]);
+                               if ($sigblock) {
+                                       $keyId = $sigblock['keyId'];
+
+                                       if ($keyId) {
+                                               // Try to find the public contact entry of the handle.
+                                               $handle = str_replace("acct:", "", $keyId);
+                                               $fields = ["id", "url", "addr", "pubkey"];
+                                               $condition = ["addr" => $handle, "uid" => 0];
+
+                                               $contact = dba::selectFirst("contact", $fields, $condition);
+
+                                               // Not found? Try to probe with the handle.
+                                               if(!DBM::is_result($contact)) {
+                                                       Probe::uri($handle, '', -1, true, true);
+                                                       $contact = dba::selectFirst("contact", $fields, $condition);
+                                               }
+
+                                               if (DBM::is_result($contact)) {
+                                                       // Try to verify the signed header with the public key of the contact record
+                                                       // we have found.
+                                                       $verified = HTTPSig::verify('', $contact['pubkey']);
+
+                                                       if ($verified && $verified['header_signed'] && $verified['header_valid']) {
+                                                               logger('OWA header: ' . print_r($verified, true), LOGGER_DATA);
+                                                               logger('OWA success: ' . $contact['addr'], LOGGER_DATA);
+
+                                                               $ret['success'] = true;
+                                                               $token = random_string(32);
+
+                                                               // Store the generated token in the databe.
+                                                               Verify::create('owt', 0, $token, $contact['addr']);
+
+                                                               $result = '';
+
+                                                               // Encrypt the token with the public contacts publik key.
+                                                               // Only the specific public contact will be able to encrypt it.
+                                                               // At a later time, we will compare weather the token we're getting
+                                                               // is really the same token we have stored in the database.
+                                                               openssl_public_encrypt($token, $result, $contact['pubkey']);
+                                                               $ret['encrypted_token'] = base64url_encode($result);
+                                                       } else {
+                                                               logger('OWA fail: ' . $contact['id'] . ' ' . $contact['addr'] . ' ' . $contact['url'], LOGGER_DEBUG);
+                                                       }
+                                               } else {
+                                                       logger('Contact not found: ' . $handle, LOGGER_DEBUG);
+                                               }
+                                       }
+                               }
+                       }
+               }
+               System::jsonExit($ret, 'application/x-dfrn+json');
+       }
+}
index 5f66581..7f41b23 100644 (file)
@@ -311,10 +311,11 @@ class Probe
         * @param string  $network Test for this specific network
         * @param integer $uid     User ID for the probe (only used for mails)
         * @param boolean $cache   Use cached values?
+        * @param boolean $insert  Insert the contact into the contact table.
         *
         * @return array uri data
         */
-       public static function uri($uri, $network = "", $uid = -1, $cache = true)
+       public static function uri($uri, $network = "", $uid = -1, $cache = true, $insert = false)
        {
                if ($cache) {
                        $result = Cache::get("Probe::uri:".$network.":".$uri);
@@ -463,11 +464,19 @@ class Probe
                                $condition = ['nurl' => normalise_link($data["url"]), 'self' => false, 'uid' => 0];
 
                                // "$old_fields" will return a "false" when the contact doesn't exist.
-                               // This won't trigger an insert. This is intended, since we only need
-                               // public contacts for everyone we store items from.
-                               // We don't need to store every contact on the planet.
+                               // This won't trigger an insert except $insert is set to true.
+                               // This is intended, since we only need public contacts
+                               // for everyone we store items from. We don't need to store
+                               // every contact on the planet.
                                $old_fields = dba::selectFirst('contact', $fieldnames, $condition);
 
+                               // When the contact doesn't exist, the value "true" will trigger an insert
+                               if (!$old_fields && $insert) {
+                                       $old_fields = true;
+                                       $fields['blocked'] = false;
+                                       $fields['pending'] = false;
+                               }
+
                                $fields['name-date'] = DateTimeFormat::utcNow();
                                $fields['uri-date'] = DateTimeFormat::utcNow();
                                $fields['success_update'] = DateTimeFormat::utcNow();
index b2fad99..2dc9783 100644 (file)
@@ -4,6 +4,7 @@
  */
 namespace Friendica\Util;
 
+use Friendica\Core\Addon;
 use Friendica\Core\Config;
 use ASN_BASE;
 use ASNValue;
@@ -246,4 +247,221 @@ class Crypto
 
                return $response;
        }
+
+       /**
+        * Encrypt a string with 'aes-256-cbc' cipher method.
+        * 
+        * @param string $data
+        * @param string $key   The key used for encryption.
+        * @param string $iv    A non-NULL Initialization Vector.
+        * 
+        * @return string|boolean Encrypted string or false on failure.
+        */
+       private static function encryptAES256CBC($data, $key, $iv)
+       {
+               return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
+       }
+
+       /**
+        * Decrypt a string with 'aes-256-cbc' cipher method.
+        * 
+        * @param string $data
+        * @param string $key   The key used for decryption.
+        * @param string $iv    A non-NULL Initialization Vector.
+        * 
+        * @return string|boolean Decrypted string or false on failure.
+        */
+       private static function decryptAES256CBC($data, $key, $iv)
+       {
+               return openssl_decrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
+       }
+
+       /**
+        * Encrypt a string with 'aes-256-ctr' cipher method.
+        * 
+        * @param string $data
+        * @param string $key   The key used for encryption.
+        * @param string $iv    A non-NULL Initialization Vector.
+        * 
+        * @return string|boolean Encrypted string or false on failure.
+        */
+       private static function encryptAES256CTR($data, $key, $iv)
+       {
+               $key = substr($key, 0, 32);
+               $iv = substr($iv, 0, 16);
+               return openssl_encrypt($data, 'aes-256-ctr', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
+       }
+
+       /**
+        * Decrypt a string with 'aes-256-cbc' cipher method.
+        * 
+        * @param string $data
+        * @param string $key   The key used for decryption.
+        * @param string $iv    A non-NULL Initialization Vector.
+        * 
+        * @return string|boolean Decrypted string or false on failure.
+        */
+       private static function decryptAES256CTR($data, $key, $iv)
+       {
+               $key = substr($key, 0, 32);
+               $iv = substr($iv, 0, 16);
+               return openssl_decrypt($data, 'aes-256-ctr', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
+       }
+
+       /**
+        * 
+        * @param string $data
+        * @param string $pubkey The public key.
+        * @param string $alg    The algorithm used for encryption.
+        * 
+        * @return array
+        */
+       public static function encapsulate($data, $pubkey, $alg = 'aes256cbc')
+       {
+               if ($alg === 'aes256cbc') {
+                       return self::encapsulateAes($data, $pubkey);
+               }
+               return self::encapsulateOther($data, $pubkey, $alg);
+       }
+
+       /**
+        * 
+        * @param type $data
+        * @param type $pubkey The public key.
+        * @param type $alg    The algorithm used for encryption.
+        * 
+        * @return array
+        */
+       private static function encapsulateOther($data, $pubkey, $alg)
+       {
+               if (!$pubkey) {
+                       logger('no key. data: '.$data);
+               }
+               $fn = 'encrypt' . strtoupper($alg);
+               if (method_exists(__CLASS__, $fn)) {
+                       // A bit hesitant to use openssl_random_pseudo_bytes() as we know
+                       // it has been historically targeted by US agencies for 'weakening'.
+                       // It is still arguably better than trying to come up with an
+                       // alternative cryptographically secure random generator.
+                       // There is little point in using the optional second arg to flag the
+                       // assurance of security since it is meaningless if the source algorithms
+                       // have been compromised. Also none of this matters if RSA has been
+                       // compromised by state actors and evidence is mounting that this has
+                       // already happened.
+                       $result = ['encrypted' => true];
+                       $key = openssl_random_pseudo_bytes(256);
+                       $iv  = openssl_random_pseudo_bytes(256);
+                       $result['data'] = base64url_encode(self::$fn($data, $key, $iv), true);
+
+                       // log the offending call so we can track it down
+                       if (!openssl_public_encrypt($key, $k, $pubkey)) {
+                               $x = debug_backtrace();
+                               logger('RSA failed. ' . print_r($x[0], true));
+                       }
+
+                       $result['alg'] = $alg;
+                       $result['key'] = base64url_encode($k, true);
+                       openssl_public_encrypt($iv, $i, $pubkey);
+                       $result['iv'] = base64url_encode($i, true);
+
+                       return $result;
+               } else {
+                       $x = ['data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data];
+                       Addon::callHooks('other_encapsulate', $x);
+
+                       return $x['result'];
+               }
+       }
+
+       /**
+        * 
+        * @param string $data
+        * @param string $pubkey
+        * 
+        * @return array
+        */
+       private static function encapsulateAes($data, $pubkey)
+       {
+               if (!$pubkey) {
+                       logger('aes_encapsulate: no key. data: ' . $data);
+               }
+
+               $key = openssl_random_pseudo_bytes(32);
+               $iv  = openssl_random_pseudo_bytes(16);
+               $result = ['encrypted' => true];
+               $result['data'] = base64url_encode(AES256CBC_encrypt($data, $key, $iv), true);
+
+               // log the offending call so we can track it down
+               if (!openssl_public_encrypt($key, $k, $pubkey)) {
+                       $x = debug_backtrace();
+                       logger('aes_encapsulate: RSA failed. ' . print_r($x[0], true));
+               }
+
+               $result['alg'] = 'aes256cbc';
+               $result['key'] = base64url_encode($k, true);
+               openssl_public_encrypt($iv, $i, $pubkey);
+               $result['iv'] = base64url_encode($i, true);
+
+               return $result;
+       }
+
+       /**
+        * 
+        * @param string $data
+        * @param string $prvkey  The private key used for decryption.
+        * 
+        * @return string|boolean The decrypted string or false on failure.
+        */
+       public static function unencapsulate($data, $prvkey)
+       {
+               if (!$data) {
+                       return;
+               }
+
+               $alg = ((array_key_exists('alg', $data)) ? $data['alg'] : 'aes256cbc');
+               if ($alg === 'aes256cbc') {
+                       return self::encapsulateAes($data, $prvkey);
+               }
+               return self::encapsulateOther($data, $prvkey, $alg);
+       }
+
+       /**
+        * 
+        * @param string $data
+        * @param string $prvkey  The private key used for decryption.
+        * @param string $alg
+        * 
+        * @return string|boolean The decrypted string or false on failure.
+        */
+       private static function unencapsulateOther($data, $prvkey, $alg)
+       {
+               $fn = 'decrypt' . strtoupper($alg);
+
+               if (method_exists(__CLASS__, $fn)) {
+                       openssl_private_decrypt(base64url_decode($data['key']), $k, $prvkey);
+                       openssl_private_decrypt(base64url_decode($data['iv']), $i, $prvkey);
+
+                       return self::$fn(base64url_decode($data['data']), $k, $i);
+               } else {
+                       $x = ['data' => $data, 'prvkey' => $prvkey, 'alg' => $alg, 'result' => $data];
+                       Addon::callHooks('other_unencapsulate', $x);
+
+                       return $x['result'];
+               }
+       }
+
+       /**
+        * 
+        * @param array  $data
+        * @param string $prvkey  The private key used for decryption.
+        * 
+        * @return string|boolean The decrypted string or false on failure.
+        */
+       private static function unencapsulateAes($data, $prvkey)
+       {
+               openssl_private_decrypt(base64url_decode($data['key']), $k, $prvkey);
+               openssl_private_decrypt(base64url_decode($data['iv']), $i, $prvkey);
+
+               return self::decryptAES256CBC(base64url_decode($data['data']), $k, $i);
+       }
 }
diff --git a/src/Util/HTTPHeaders.php b/src/Util/HTTPHeaders.php
new file mode 100644 (file)
index 0000000..a6c270d
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * @file src/Util/HTTPHeaders.php
+ */
+namespace Friendica\Util;
+
+class HTTPHeaders
+{
+       private $in_progress = [];
+       private $parsed = [];
+
+       function __construct($headers)
+       {
+               $lines = explode("\n", str_replace("\r", '', $headers));
+
+               if ($lines) {
+                       foreach ($lines as $line) {
+                               if (preg_match('/^\s+/', $line, $matches) && trim($line)) {
+                                       if ($this->in_progress['k']) {
+                                               $this->in_progress['v'] .= ' ' . ltrim($line);
+                                               continue;
+                                       }
+                               } else {
+                                       if ($this->in_progress['k']) {
+                                               $this->parsed[] = [$this->in_progress['k'] => $this->in_progress['v']];
+                                               $this->in_progress = [];
+                                       }
+
+                                       $this->in_progress['k'] = strtolower(substr($line, 0, strpos($line, ':')));
+                                       $this->in_progress['v'] = ltrim(substr($line, strpos($line, ':') + 1));
+                               }
+                       }
+
+                       if ($this->in_progress['k']) {
+                               $this->parsed[] = [$this->in_progress['k'] => $this->in_progress['v']];
+                               $this->in_progress = [];
+                       }
+               }
+       }
+
+       function fetch()
+       {
+               return $this->parsed;
+       }
+
+       function fetcharr()
+       {
+               $ret = [];
+
+               if ($this->parsed) {
+                       foreach ($this->parsed as $x) {
+                               foreach ($x as $y => $z) {
+                                       $ret[$y] = $z;
+                               }
+                       }
+               }
+               return $ret;
+       }
+}
diff --git a/src/Util/HTTPSig.php b/src/Util/HTTPSig.php
new file mode 100644 (file)
index 0000000..a7c9f23
--- /dev/null
@@ -0,0 +1,352 @@
+<?php
+
+/**
+ * @file src/Util/HTTPSig.php
+ */
+namespace Friendica\Util;
+
+use Friendica\Core\Config;
+use Friendica\Util\Crypto;
+use Friendica\Util\HTTPHeaders;
+
+/**
+ * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07.
+ *
+ * @see https://tools.ietf.org/html/draft-cavage-http-signatures-07
+ */
+
+class HTTPSig
+{
+       /**
+        * @brief RFC5843
+        *
+        * @see https://tools.ietf.org/html/rfc5843
+        *
+        * @param string $body The value to create the digest for
+        * @param boolean $set (optional, default true)
+        *   If set send a Digest HTTP header
+        * @return string The generated digest of $body
+        */
+       public static function generateDigest($body, $set = true)
+       {
+               $digest = base64_encode(hash('sha256', $body, true));
+
+               if($set) {
+                       header('Digest: SHA-256=' . $digest);
+               }
+               return $digest;
+       }
+
+       // See draft-cavage-http-signatures-08
+       public static function verify($data, $key = '')
+       {
+               $body      = $data;
+               $headers   = null;
+               $spoofable = false;
+               $result = [
+                       'signer'         => '',
+                       'header_signed'  => false,
+                       'header_valid'   => false,
+                       'content_signed' => false,
+                       'content_valid'  => false
+               ];
+
+               // Decide if $data arrived via controller submission or curl.
+               if (is_array($data) && $data['header']) {
+                       if (!$data['success']) {
+                               return $result;
+                       }
+
+                       $h = new HTTPHeaders($data['header']);
+                       $headers = $h->fetcharr();
+                       $body = $data['body'];
+               } else {
+                       $headers = [];
+                       $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI'];
+
+                       foreach ($_SERVER as $k => $v) {
+                               if (strpos($k, 'HTTP_') === 0) {
+                                       $field = str_replace('_', '-', strtolower(substr($k, 5)));
+                                       $headers[$field] = $v;
+                               }
+                       }
+               }
+
+               $sig_block = null;
+
+               if (array_key_exists('signature', $headers)) {
+                       $sig_block = self::parseSigheader($headers['signature']);
+               } elseif (array_key_exists('authorization', $headers)) {
+                       $sig_block = self::parseSigheader($headers['authorization']);
+               }
+
+               if (!$sig_block) {
+                       logger('no signature provided.');
+                       return $result;
+               }
+
+               // Warning: This log statement includes binary data
+               // logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA);
+
+               $result['header_signed'] = true;
+
+               $signed_headers = $sig_block['headers'];
+               if (!$signed_headers) {
+                       $signed_headers = ['date'];
+               }
+
+               $signed_data = '';
+               foreach ($signed_headers as $h) {
+                       if (array_key_exists($h, $headers)) {
+                               $signed_data .= $h . ': ' . $headers[$h] . "\n";
+                       }
+                       if (strpos($h, '.')) {
+                               $spoofable = true;
+                       }
+               }
+
+               $signed_data = rtrim($signed_data, "\n");
+
+               $algorithm = null;
+               if ($sig_block['algorithm'] === 'rsa-sha256') {
+                       $algorithm = 'sha256';
+               }
+               if ($sig_block['algorithm'] === 'rsa-sha512') {
+                       $algorithm = 'sha512';
+               }
+
+               if ($key && function_exists($key)) { /// @todo What function do we check for - maybe we check now for a method !!!
+                       $result['signer'] = $sig_block['keyId'];
+                       $key = $key($sig_block['keyId']);
+               }
+
+               if (!$key) {
+                       return $result;
+               }
+
+               $x = Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm);
+
+               logger('verified: ' . $x, LOGGER_DEBUG);
+
+               if (!$x) {
+                       return $result;
+               }
+
+               if (!$spoofable) {
+                       $result['header_valid'] = true;
+               }
+
+               if (in_array('digest', $signed_headers)) {
+                       $result['content_signed'] = true;
+                       $digest = explode('=', $headers['digest']);
+
+                       if ($digest[0] === 'SHA-256') {
+                               $hashalg = 'sha256';
+                       }
+                       if ($digest[0] === 'SHA-512') {
+                               $hashalg = 'sha512';
+                       }
+
+                       // The explode operation will have stripped the '=' padding, so compare against unpadded base64.
+                       if (rtrim(base64_encode(hash($hashalg, $body, true)), '=') === $digest[1]) {
+                               $result['content_valid'] = true;
+                       }
+               }
+
+               logger('Content_Valid: ' . $result['content_valid']);
+
+               return $result;
+       }
+
+       /**
+        * @brief
+        *
+        * @param string  $request
+        * @param array   $head
+        * @param string  $prvkey
+        * @param string  $keyid (optional, default 'Key')
+        * @param boolean $send_headers (optional, default false)
+        *   If set send a HTTP header
+        * @param boolean $auth (optional, default false)
+        * @param string  $alg (optional, default 'sha256')
+        * @param string  $crypt_key (optional, default null)
+        * @param string  $crypt_algo (optional, default 'aes256ctr')
+        * 
+        * @return array
+        */
+       public static function createSig($request, $head, $prvkey, $keyid = 'Key', $send_headers = false, $auth = false, $alg = 'sha256', $crypt_key = null, $crypt_algo = 'aes256ctr')
+       {
+               $return_headers = [];
+
+               if ($alg === 'sha256') {
+                       $algorithm = 'rsa-sha256';
+               }
+
+               if ($alg === 'sha512') {
+                       $algorithm = 'rsa-sha512';
+               }
+
+               $x = self::sign($request, $head, $prvkey, $alg);
+
+               $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm
+                       . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"';
+
+               if ($crypt_key) {
+                       $x = Crypto::encapsulate($headerval, $crypt_key, $crypt_algo);
+                       $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"';
+               }
+
+               if ($auth) {
+                       $sighead = 'Authorization: Signature ' . $headerval;
+               } else {
+                       $sighead = 'Signature: ' . $headerval;
+               }
+
+               if ($head) {
+                       foreach ($head as $k => $v) {
+                               if ($send_headers) {
+                                       header($k . ': ' . $v);
+                               } else {
+                                       $return_headers[] = $k . ': ' . $v;
+                               }
+                       }
+               }
+
+               if ($send_headers) {
+                       header($sighead);
+               } else {
+                       $return_headers[] = $sighead;
+               }
+
+               return $return_headers;
+       }
+
+       /**
+        * @brief
+        *
+        * @param string $request
+        * @param array  $head
+        * @param string $prvkey
+        * @param string $alg (optional) default 'sha256'
+        * 
+        * @return array
+        */
+       private static function sign($request, $head, $prvkey, $alg = 'sha256')
+       {
+               $ret = [];
+               $headers = '';
+               $fields  = '';
+
+               if ($request) {
+                       $headers = '(request-target)' . ': ' . trim($request) . "\n";
+                       $fields = '(request-target)';
+               }
+
+               if ($head) {
+                       foreach ($head as $k => $v) {
+                               $headers .= strtolower($k) . ': ' . trim($v) . "\n";
+                               if ($fields) {
+                                       $fields .= ' ';
+                               }
+                               $fields .= strtolower($k);
+                       }
+                       // strip the trailing linefeed
+                       $headers = rtrim($headers, "\n");
+               }
+
+               $sig = base64_encode(Crypto::rsaSign($headers, $prvkey, $alg));
+
+               $ret['headers']   = $fields;
+               $ret['signature'] = $sig;
+       
+               return $ret;
+       }
+
+       /**
+        * @brief
+        *
+        * @param string $header
+        * @return array associate array with
+        *   - \e string \b keyID
+        *   - \e string \b algorithm
+        *   - \e array  \b headers
+        *   - \e string \b signature
+        */
+       public static function parseSigheader($header)
+       {
+               $ret = [];
+               $matches = [];
+
+               // if the header is encrypted, decrypt with (default) site private key and continue
+               if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
+                       $header = self::decryptSigheader($header);
+               }
+
+               if (preg_match('/keyId="(.*?)"/ism', $header, $matches)) {
+                       $ret['keyId'] = $matches[1];
+               }
+
+               if (preg_match('/algorithm="(.*?)"/ism', $header, $matches)) {
+                       $ret['algorithm'] = $matches[1];
+               }
+
+               if (preg_match('/headers="(.*?)"/ism', $header, $matches)) {
+                       $ret['headers'] = explode(' ', $matches[1]);
+               }
+
+               if (preg_match('/signature="(.*?)"/ism', $header, $matches)) {
+                       $ret['signature'] = base64_decode(preg_replace('/\s+/', '', $matches[1]));
+               }
+
+               if (($ret['signature']) && ($ret['algorithm']) && (!$ret['headers'])) {
+                       $ret['headers'] = ['date'];
+               }
+
+               return $ret;
+       }
+
+       /**
+        * @brief
+        *
+        * @param string $header
+        * @param string $prvkey (optional), if not set use site private key
+        * 
+        * @return array|string associative array, empty string if failue
+        *   - \e string \b iv
+        *   - \e string \b key
+        *   - \e string \b alg
+        *   - \e string \b data
+        */
+       private static function decryptSigheader($header, $prvkey = null)
+       {
+               $iv = $key = $alg = $data = null;
+
+               if (!$prvkey) {
+                       $prvkey = Config::get('system', 'prvkey');
+               }
+
+               $matches = [];
+
+               if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
+                       $iv = $matches[1];
+               }
+
+               if (preg_match('/key="(.*?)"/ism', $header, $matches)) {
+                       $key = $matches[1];
+               }
+
+               if (preg_match('/alg="(.*?)"/ism', $header, $matches)) {
+                       $alg = $matches[1];
+               }
+
+               if (preg_match('/data="(.*?)"/ism', $header, $matches)) {
+                       $data = $matches[1];
+               }
+
+               if ($iv && $key && $alg && $data) {
+                       return Crypto::unencapsulate(['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data], $prvkey);
+               }
+
+               return '';
+       }
+}
index 360489b..aa402b1 100644 (file)
@@ -33,4 +33,7 @@
           template="{{$subscribe}}" />
     <Link rel="magic-public-key" 
           href="{{$modexp}}" />
+    <Link rel="http://purl.org/openwebauth/v1"
+          type="application/x-dfrn+json"
+          href="{{$openwebauth}}" />
 </XRD>