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);
/**
-- ------------------------------------------
-- Friendica 2018.08-dev (The Tazmans Flax-lily)
--- DB_UPDATE_VERSION 1268
+-- DB_UPDATE_VERSION 1269
-- ------------------------------------------
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`)
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
--
'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
-------------
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
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
['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();
'$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]
);
}
/**
- * @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();
}
"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" => [
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;
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)
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);
+ }
}
--- /dev/null
+<?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);
+ }
+
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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');
+ }
+}
* @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);
$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();
*/
namespace Friendica\Util;
+use Friendica\Core\Addon;
use Friendica\Core\Config;
use ASN_BASE;
use ASNValue;
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);
+ }
}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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 '';
+ }
+}
template="{{$subscribe}}" />
<Link rel="magic-public-key"
href="{{$modexp}}" />
+ <Link rel="http://purl.org/openwebauth/v1"
+ type="application/x-dfrn+json"
+ href="{{$openwebauth}}" />
</XRD>