From bb8b3b92913832750a393533f491725b4330a8e1 Mon Sep 17 00:00:00 2001 From: zotlabs Date: Tue, 6 Nov 2018 20:44:40 -0800 Subject: this is brutal --- Zotlabs/Lib/ActivityStreams.php | 191 +++++++++++++++++++++++++++++++++++----- Zotlabs/Lib/JSalmon.php | 50 +++++++++-- Zotlabs/Lib/Libzot.php | 74 +++++++++------- Zotlabs/Lib/Zotfinger.php | 23 +++-- 4 files changed, 270 insertions(+), 68 deletions(-) (limited to 'Zotlabs/Lib') diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index 37e717f58..a322637fd 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -7,22 +7,25 @@ namespace Zotlabs\Lib; * * Parses an ActivityStream JSON string. */ + class ActivityStreams { - public $raw = null; - public $data; - public $valid = false; - public $id = ''; - public $type = ''; - public $actor = null; - public $obj = null; - public $tgt = null; - public $origin = null; - public $owner = null; - public $signer = null; - public $ldsig = null; - public $sigok = false; - public $recips = null; + public $raw = null; + public $data = null; + public $valid = false; + public $deleted = false; + public $id = ''; + public $parent_id = ''; + public $type = ''; + public $actor = null; + public $obj = null; + public $tgt = null; + public $origin = null; + public $owner = null; + public $signer = null; + public $ldsig = null; + public $sigok = false; + public $recips = null; public $raw_recips = null; /** @@ -35,16 +38,49 @@ class ActivityStreams { function __construct($string) { $this->raw = $string; - $this->data = json_decode($string, true); + + if(is_array($string)) { + $this->data = $string; + } + else { + $this->data = json_decode($string, true); + } if($this->data) { + + // verify and unpack JSalmon signature if present + + if(is_array($this->data) && array_key_exists('signed',$this->data)) { + $ret = JSalmon::verify($this->data); + $tmp = JSalmon::unpack($this->data['data']); + if($ret && $ret['success']) { + if($ret['signer']) { + $saved = json_encode($this->data,JSON_UNESCAPED_SLASHES); + $this->data = $tmp; + $this->data['signer'] = $ret['signer']; + $this->data['signed_data'] = $saved; + if($ret['hubloc']) { + $this->data['hubloc'] = $ret['hubloc']; + } + } + } + } + $this->valid = true; + + if(array_key_exists('type',$this->data) && array_key_exists('actor',$this->data) && array_key_exists('object',$this->data)) { + if($this->data['type'] === 'Delete' && $this->data['actor'] === $this->data['object']) { + $this->deleted = $this->data['actor']; + $this->valid = false; + } + } + } if($this->is_valid()) { $this->id = $this->get_property_obj('id'); $this->type = $this->get_primary_type(); - $this->actor = $this->get_compound_property('actor'); + $this->actor = $this->get_actor('actor','',''); $this->obj = $this->get_compound_property('object'); $this->tgt = $this->get_compound_property('target'); $this->origin = $this->get_compound_property('origin'); @@ -53,14 +89,31 @@ class ActivityStreams { $this->ldsig = $this->get_compound_property('signature'); if($this->ldsig) { $this->signer = $this->get_compound_property('creator',$this->ldsig); - if($this->signer && $this->signer['publicKey'] && $this->signer['publicKey']['publicKeyPem']) { - $this->sigok = \Zotlabs\Lib\LDSignatures::verify($this->data,$this->signer['publicKey']['publicKeyPem']); + if($this->signer && is_array($this->signer) && array_key_exists('publicKey',$this->signer) && is_array($this->signer['publicKey']) && $this->signer['publicKey']['publicKeyPem']) { + $this->sigok = LDSignatures::verify($this->data,$this->signer['publicKey']['publicKeyPem']); } } - if(($this->type === 'Note') && (! $this->obj)) { + if(! $this->obj) { $this->obj = $this->data; $this->type = 'Create'; + if(! $this->actor) { + $this->actor = $this->get_actor('attributedTo',$this->obj); + } + } + + if($this->obj && is_array($this->obj) && $this->obj['actor']) + $this->obj['actor'] = $this->get_actor('actor',$this->obj); + if($this->tgt && is_array($this->tgt) && $this->tgt['actor']) + $this->tgt['actor'] = $this->get_actor('actor',$this->tgt); + + $this->parent_id = $this->get_property_obj('inReplyTo'); + + if((! $this->parent_id) && is_array($this->obj)) { + $this->parent_id = $this->obj['inReplyTo']; + } + if((! $this->parent_id) && is_array($this->obj)) { + $this->parent_id = $this->obj['id']; } } } @@ -190,44 +243,122 @@ class ActivityStreams { $base = (($base) ? $base : $this->data); $propname = (($prefix) ? $prefix . ':' : '') . $property; + if(! is_array($base)) { + btlogger('not an array: ' . print_r($base,true)); + return null; + } + return ((array_key_exists($propname, $base)) ? $base[$propname] : null); } + /** * @brief Fetches a property from an URL. * * @param string $url * @return NULL|mixed */ + function fetch_property($url) { + return self::fetch($url); + } + + static function fetch($url) { $redirects = 0; if(! check_siteallowed($url)) { logger('blacklisted: ' . $url); return null; } - + logger('fetch: ' . $url, LOGGER_DEBUG); $x = z_fetch_url($url, true, $redirects, - ['headers' => [ 'Accept: application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ]]); - if($x['success']) + [ 'headers' => [ 'Accept: application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ]]); + if($x['success']) { + $y = json_decode($x['body'],true); + logger('returned: ' . json_encode($y,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); return json_decode($x['body'], true); + } + else { + logger('fetch failed: ' . $url); + } + return null; + } + + static function is_an_actor($s) { + return(in_array($s,[ 'Application','Group','Service','Person','Service' ])); + } + + /** + * @brief + * + * @param string $property + * @param array $base + * @param string $namespace (optional) default empty + * @return NULL|mixed + */ + + function get_actor($property,$base='',$namespace = '') { + $x = $this->get_property_obj($property, $base, $namespace); + if($this->is_url($x)) { + + // SECURITY: If we have already stored the actor profile, re-generate it + // from cached data - don't refetch it from the network + $r = q("select * from xchan left join hubloc on xchan_hash = hubloc_hash where hubloc_id_url = '%s' limit 1", + dbesc($x) + ); + if($r) { + $y = Activity::encode_person($r[0]); + $y['cached'] = true; + return $y; + } + } + $actor = $this->get_compound_property($property,$base,$namespace,true); + if(is_array($actor) && self::is_an_actor($actor['type'])) { + if(array_key_exists('id',$actor) && (! array_key_exists('inbox',$actor))) { + $actor = $this->fetch_property($actor['id']); + } + return $actor; + } return null; } + /** * @brief * * @param string $property * @param array $base * @param string $namespace (optional) default empty + * @param boolean $first (optional) default false, if true and result is a sequential array return only the first element * @return NULL|mixed */ - function get_compound_property($property, $base = '', $namespace = '') { + function get_compound_property($property, $base = '', $namespace = '', $first = false) { $x = $this->get_property_obj($property, $base, $namespace); if($this->is_url($x)) { $x = $this->fetch_property($x); } + // verify and unpack JSalmon signature if present + + if(is_array($x) && array_key_exists('signed',$x)) { + $ret = JSalmon::verify($x); + $tmp = JSalmon::unpack($x['data']); + if($ret && $ret['success']) { + if($ret['signer']) { + $saved = json_encode($x,JSON_UNESCAPED_SLASHES); + $x = $tmp; + $x['signer'] = $ret['signer']; + $x['signed_data'] = $saved; + if($ret['hubloc']) { + $x['hubloc'] = $ret['hubloc']; + } + } + } + } + if($first && is_array($x) && array_key_exists(0,$x)) { + return $x[0]; + } + return $x; } @@ -273,4 +404,18 @@ class ActivityStreams { return $x; } + + static function is_as_request() { + + $x = getBestSupportedMimeType([ + 'application/ld+json;profile="https://www.w3.org/ns/activitystreams"', + 'application/activity+json', + 'application/ld+json;profile="http://www.w3.org/ns/activitystreams"' + ]); + + return(($x) ? true : false); + + } + + } \ No newline at end of file diff --git a/Zotlabs/Lib/JSalmon.php b/Zotlabs/Lib/JSalmon.php index 43d5f9d09..f35bf6235 100644 --- a/Zotlabs/Lib/JSalmon.php +++ b/Zotlabs/Lib/JSalmon.php @@ -2,15 +2,13 @@ namespace Zotlabs\Lib; +use Zotlabs\Zot6\HTTPSig; class JSalmon { - static function sign($data,$key_id,$key) { + static function sign($data,$key_id,$key,$data_type = 'application/x-zot+json') { - $arr = $data; - $data = json_encode($data,JSON_UNESCAPED_SLASHES); - $data = base64url_encode($data, false); // do not strip padding - $data_type = 'application/x-zot+json'; + $data = base64url_encode(json_encode($data,true),true); // strip padding $encoding = 'base64url'; $algorithm = 'RSA-SHA256'; @@ -18,9 +16,9 @@ class JSalmon { // precomputed base64url encoding of data_type, encoding, algorithm concatenated with periods - $precomputed = '.' . base64url_encode($data_type,false) . '.YmFzZTY0dXJs.UlNBLVNIQTI1Ng=='; + $precomputed = '.' . base64url_encode($data_type,true) . '.YmFzZTY0dXJs.UlNBLVNIQTI1Ng'; - $signature = base64url_encode(rsa_sign($data . $precomputed, $key), false); + $signature = base64url_encode(rsa_sign($data . $precomputed, $key), true); return ([ 'signed' => true, @@ -30,9 +28,45 @@ class JSalmon { 'alg' => $algorithm, 'sigs' => [ 'value' => $signature, - 'key_id' => base64url_encode($key_id) + 'key_id' => base64url_encode($key_id, true) ] ]); } + + static function verify($x) { + + logger('verify'); + $ret = [ 'results' => [] ]; + + if(! is_array($x)) { + return $false; + } + if(! ( array_key_exists('signed',$x) && $x['signed'])) { + return $false; + } + + $signed_data = preg_replace('/\s+/','',$x['data']) . '.' + . base64url_encode($x['data_type'],true) . '.' + . base64url_encode($x['encoding'],true) . '.' + . base64url_encode($x['alg'],true); + + $key = HTTPSig::get_key(EMPTY_STR,base64url_decode($x['sigs']['key_id'])); + logger('key: ' . print_r($key,true)); + if($key['portable_id'] && $key['public_key']) { + if(rsa_verify($signed_data,base64url_decode($x['sigs']['value']),$key['public_key'])) { + logger('verified'); + $ret = [ 'success' => true, 'signer' => $key['portable_id'], 'hubloc' => $key['hubloc'] ]; + } + } + + return $ret; + + } + + static function unpack($data) { + return json_decode(base64url_decode($data),true); + } + + } \ No newline at end of file diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 3d18df051..23703a040 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -109,7 +109,7 @@ class Libzot { $data = [ 'type' => $type, 'encoding' => $encoding, - 'sender' => $channel['channel_hash'], + 'sender' => $channel['channel_portable_id'], 'site_id' => self::make_xchan_hash(z_root(), get_config('system','pubkey')), 'version' => System::get_zot_revision(), ]; @@ -324,14 +324,19 @@ class Libzot { logger('zot-info: ' . print_r($record,true), LOGGER_DATA, LOG_DEBUG); $x = self::import_xchan($record['data'], (($force) ? UPDATE_FLAGS_FORCED : UPDATE_FLAGS_UPDATED)); -logger('1'); + if(! $x['success']) return false; -logger('2'); + if($channel && $record['data']['permissions']) { -logger('3'); - $old_read_stream_perm = their_perms_contains($channel['channel_id'],$x['hash'],'view_stream'); - set_abconfig($channel['channel_id'],$x['hash'],'system','their_perms',$record['data']['permissions']); + $permissions = explode(',',$record['data']['permissions']); + if($permissions && is_array($permissions)) { + $old_read_stream_perm = get_abconfig($channel['channel_id'],$x['hash'],'their_perms','view_stream'); + + foreach($permissions as $k => $v) { + set_abconfig($channel['channel_id'],$x['hash'],'their_perms',$k,$v); + } + } if(array_key_exists('profile',$record['data']) && array_key_exists('next_birthday',$record['data']['profile'])) { $next_birthday = datetime_convert('UTC','UTC',$record['data']['profile']['next_birthday']); @@ -380,14 +385,16 @@ logger('4'); else { $p = Permissions::connect_perms($channel['channel_id']); - $my_perms = Permissions::serialise($p['perms']); + $my_perms = $p['perms']; $automatic = $p['automatic']; // new connection if($my_perms) { - set_abconfig($channel['channel_id'],$x['hash'],'system','my_perms',$my_perms); + foreach($my_perms as $k => $v) { + set_abconfig($channel['channel_id'],$x['hash'],'my_perms',$k,$v); + } } $closeness = get_pconfig($channel['channel_id'],'system','new_abook_closeness'); @@ -410,7 +417,7 @@ logger('4'); if($y) { logger("New introduction received for {$channel['channel_name']}"); - $new_perms = get_all_perms($channel['channel_id'],$x['hash']); + $new_perms = get_all_perms($channel['channel_id'],$x['hash'],false); // Send a clone sync packet and a permissions update if permissions have changed @@ -426,7 +433,7 @@ logger('4'); [ 'type' => NOTIFY_INTRO, 'from_xchan' => $x['hash'], - 'to_xchan' => $channel['channel_hash'], + 'to_xchan' => $channel['channel_portable_id'], 'link' => z_root() . '/connedit/' . $new_connection[0]['abook_id'] ] ); @@ -777,7 +784,7 @@ logger('4'); // see if this is a channel clone that's hosted locally - which we treat different from other xchans/connections - $local = q("select channel_account_id, channel_id from channel where channel_hash = '%s' limit 1", + $local = q("select channel_account_id, channel_id from channel where channel_portable_id = '%s' limit 1", dbesc($xchan_hash) ); if($local) { @@ -1130,7 +1137,7 @@ logger('4'); if($recip_arr) { stringify_array_elms($recip_arr,true); $recips = implode(',',$recip_arr); - $r = q("select channel_hash as hash from channel where channel_hash in ( " . $recips . " ) and channel_removed = 0 "); + $r = q("select channel_portable_id as hash from channel where channel_portable_id in ( " . $recips . " ) and channel_removed = 0 "); } if(! $r) { @@ -1304,12 +1311,12 @@ logger('4'); $r = []; - $c = q("select channel_id, channel_hash from channel where channel_removed = 0"); + $c = q("select channel_id, channel_portable_id from channel where channel_removed = 0"); if($c) { foreach($c as $cc) { if(perm_is_allowed($cc['channel_id'],$msg['sender'],$perm)) { - $r[] = $cc['channel_hash']; + $r[] = $cc['channel_portable_id']; } } } @@ -1317,7 +1324,7 @@ logger('4'); if($include_sys) { $sys = get_sys_channel(); if($sys) - $r[] = $sys['channel_hash']; + $r[] = $sys['channel_portable_id']; } @@ -1333,7 +1340,7 @@ logger('4'); if($tag['type'] === 'Mention' && (strpos($tag['href'],z_root()) !== false)) { $address = basename($tag['href']); if($address) { - $z = q("select channel_hash as hash from channel where channel_address = '%s' + $z = q("select channel_portable_id as hash from channel where channel_address = '%s' and channel_removed = 0 limit 1", dbesc($address) ); @@ -1354,7 +1361,7 @@ logger('4'); $thread_parent = self::find_parent($msg,$act); if($thread_parent) { - $z = q("select channel_hash as hash from channel left join item on channel.channel_id = item.uid where ( item.thr_parent = '%s' OR item.parent_mid = '%s' ) ", + $z = q("select channel_portable_id as hash from channel left join item on channel.channel_id = item.uid where ( item.thr_parent = '%s' OR item.parent_mid = '%s' ) ", dbesc($thread_parent), dbesc($thread_parent) ); @@ -1428,7 +1435,7 @@ logger('4'); * access checks. */ - if($sender === $channel['channel_hash'] && $arr['author_xchan'] === $channel['channel_hash'] && $arr['mid'] === $arr['parent_mid']) { + if($sender === $channel['channel_portable_id'] && $arr['author_xchan'] === $channel['channel_portable_id'] && $arr['mid'] === $arr['parent_mid']) { $DR->update('self delivery ignored'); $result[] = $DR->get(); continue; @@ -1710,7 +1717,7 @@ logger('4'); $stored = (($item_result && $item_result['item']) ? $item_result['item'] : false); if((is_array($stored)) && ($stored['id'] != $stored['parent']) - && ($stored['author_xchan'] === $channel['channel_hash'])) { + && ($stored['author_xchan'] === $channel['channel_hash'] || $stored['author_xchan'] === $channel['channel_portable_id'])) { retain_item($stored['item']['parent']); } @@ -1810,9 +1817,9 @@ logger('4'); } logger('FOF Activity received: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG); - logger('FOF Activity recipient: ' . $channel['channel_hash'], LOGGER_DATA, LOG_DEBUG); + logger('FOF Activity recipient: ' . $channel['channel_portable_id'], LOGGER_DATA, LOG_DEBUG); - $result = self::process_delivery($arr['owner_xchan'],$arr, [ $channel['channel_hash'] ],false,false,true); + $result = self::process_delivery($arr['owner_xchan'],$arr, [ $channel['channel_portable_id'] ],false,false,true); if ($result) { $ret = array_merge($ret, $result); } @@ -2048,7 +2055,7 @@ logger('4'); $DR = new DReport(z_root(),$sender,$d,$arr['mid']); - $r = q("select * from channel where channel_hash = '%s' limit 1", + $r = q("select * from channel where channel_portable_id = '%s' limit 1", dbesc($d['hash']) ); @@ -2203,7 +2210,7 @@ logger('4'); $loc = $locations[0]; - $r = q("select * from channel where channel_hash = '%s' limit 1", + $r = q("select * from channel where channel_portable_id = '%s' limit 1", dbesc($sender_hash) ); @@ -2211,7 +2218,7 @@ logger('4'); return; if($loc['url'] !== z_root()) { - $x = q("update channel set channel_moved = '%s' where channel_hash = '%s' limit 1", + $x = q("update channel set channel_moved = '%s' where channel_portable_id = '%s' limit 1", dbesc($loc['url']), dbesc($sender_hash) ); @@ -2247,7 +2254,7 @@ logger('4'); static function encode_locations($channel) { $ret = []; - $x = self::get_hublocs($channel['channel_hash']); + $x = self::get_hublocs($channel['channel_portable_id']); if($x && count($x)) { foreach($x as $hub) { @@ -2558,6 +2565,9 @@ logger('4'); static function zotinfo($arr) { + logger('arr: ' . print_r($arr,true)); + + $ret = []; $zhash = ((x($arr,'guid_hash')) ? $arr['guid_hash'] : ''); @@ -2594,13 +2604,13 @@ logger('4'); $r = null; if(strlen($zhash)) { - $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash - where channel_hash = '%s' limit 1", + $r = q("select channel.*, xchan.* from channel left join xchan on channel_portable_id = xchan_hash + where channel_portable_id = '%s' limit 1", dbesc($zhash) ); } elseif(strlen($zguid) && strlen($zguid_sig)) { - $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash + $r = q("select channel.*, xchan.* from channel left join xchan on channel_portable_id = xchan_hash where channel_guid = '%s' and channel_guid_sig = '%s' limit 1", dbesc($zguid), dbesc($zguid_sig) @@ -2608,7 +2618,7 @@ logger('4'); } elseif(strlen($zaddr)) { if(strpos($zaddr,'[system]') === false) { /* normal address lookup */ - $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash + $r = q("select channel.*, xchan.* from channel left join xchan on channel_portable_id = xchan_hash where ( channel_address = '%s' or xchan_addr = '%s' ) limit 1", dbesc($zaddr), dbesc($zaddr) @@ -2628,10 +2638,10 @@ logger('4'); * */ - $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash + $r = q("select channel.*, xchan.* from channel left join xchan on channel_portable_id = xchan_hash where channel_system = 1 order by channel_id limit 1"); if(! $r) { - $r = q("select channel.*, xchan.* from channel left join xchan on channel_hash = xchan_hash + $r = q("select channel.*, xchan.* from channel left join xchan on channel_portable_id = xchan_hash where channel_removed = 0 order by channel_id limit 1"); } } @@ -2775,7 +2785,7 @@ logger('4'); if(! $ret['follow_url']) $ret['follow_url'] = z_root() . '/follow?f=&url=%s'; - $permissions = get_all_perms($e['channel_id'],$ztarget_hash,false); + $permissions = get_all_perms($e['channel_id'],$ztarget_hash,false,false); if($ztarget_hash) { $permissions['connected'] = false; diff --git a/Zotlabs/Lib/Zotfinger.php b/Zotlabs/Lib/Zotfinger.php index 537e440d4..d094fdc8d 100644 --- a/Zotlabs/Lib/Zotfinger.php +++ b/Zotlabs/Lib/Zotfinger.php @@ -2,7 +2,7 @@ namespace Zotlabs\Lib; -use Zotlabs\Web\HTTPSig; +use Zotlabs\Zot6\HTTPSig; class Zotfinger { @@ -12,10 +12,19 @@ class Zotfinger { return false; } - if($channel) { + $m = parse_url($resource); + + $data = json_encode([ 'zot_token' => random_string() ]); + + if($channel && $m) { + $headers = [ - 'Accept' => 'application/x-zot+json', - 'X-Zot-Token' => random_string(), + 'Accept' => 'application/x-zot+json', + 'Content-Type' => 'application/x-zot+json', + 'X-Zot-Token' => random_string(), + 'Digest' => HTTPSig::generate_digest_header($data), + 'Host' => $m['host'], + '(request-target)' => 'post ' . get_request_string($resource) ]; $h = HTTPSig::create_sig($headers,$channel['channel_prvkey'],channel_url($channel),false); } @@ -27,7 +36,9 @@ class Zotfinger { $redirects = 0; - $x = z_fetch_url($resource,false,$redirects, [ 'headers' => $h ] ); + $x = z_post_url($resource,$data,$redirects, [ 'headers' => $h ] ); + + logger('fetch: ' . print_r($x,true)); if($x['success']) { @@ -39,6 +50,8 @@ class Zotfinger { $result['data'] = json_decode(crypto_unencapsulate($result['data'],get_config('system','prvkey')),true); } + logger('decrypted: ' . print_r($result,true)); + return $result; } -- cgit v1.2.3