From 524f73e3069843484c3e257f515bc9c746aff099 Mon Sep 17 00:00:00 2001 From: Mike Macgirvin Date: Sun, 10 Oct 2010 18:25:34 -0700 Subject: [PATCH] A bit more telemetry of friend confirms and lots of extra doco to understand what is happening and why. --- mod/dfrn_confirm.php | 481 ++++++++++++++++++++++++------------------- 1 file changed, 270 insertions(+), 211 deletions(-) diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 7daa7f9443..7c24de87d0 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -1,217 +1,17 @@ argc > 1) $node = $a->argv[1]; - if(x($_POST,'source_url')) { - - // We are processing an external confirmation to an introduction created by our user. - - $public_key = $_POST['public_key']; - $dfrn_id = $_POST['dfrn_id']; - $source_url = $_POST['source_url']; - $aes_key = $_POST['aes_key']; - $duplex = $_POST['duplex']; - $version_id = $_POST['dfrn_version']; - - // Find our user's account - - $r = q("SELECT * FROM `user` WHERE `nickname` = '%s' LIMIT 1", - dbesc($node)); - - if(! count($r)) { - $message = t('No user record found for ') . '\'' . $node . '\''; - xml_status(3,$message); // failure - // NOTREACHED - } - - $my_prvkey = $r[0]['prvkey']; - $local_uid = $r[0]['uid']; - - - if(! strstr($my_prvkey,'BEGIN RSA PRIVATE KEY')) { - $message = t('Our site encryption key is apparently messed up.'); - xml_status(3,$message); - } - - // verify everything - - $decrypted_source_url = ""; - openssl_private_decrypt($source_url,$decrypted_source_url,$my_prvkey); - - - if(! strlen($decrypted_source_url)) { - $message = t('Empty site URL was provided or URL could not be decrypted by us.'); - xml_status(3,$message); - // NOTREACHED - } - - $ret = q("SELECT * FROM `contact` WHERE `url` = '%s' AND `uid` = %d LIMIT 1", - dbesc($decrypted_source_url), - intval($local_uid) - ); - - if(! count($ret)) { - // this is either a bogus confirmation (?) or we deleted the original introduction. - $message = t('Contact record was not found for you on our site.'); - xml_status(3,$message); - return; // NOTREACHED - } - - $relation = $ret[0]['rel']; - - // Decrypt all this stuff we just received - - $foreign_pubkey = $ret[0]['site-pubkey']; - $dfrn_record = $ret[0]['id']; - - $decrypted_dfrn_id = ""; - openssl_public_decrypt($dfrn_id,$decrypted_dfrn_id,$foreign_pubkey); - - if(strlen($aes_key)) { - $decrypted_aes_key = ""; - openssl_private_decrypt($aes_key,$decrypted_aes_key,$my_prvkey); - $dfrn_pubkey = openssl_decrypt($public_key,'AES-256-CBC',$decrypted_aes_key); - } - else { - $dfrn_pubkey = $public_key; - } - - $r = q("SELECT * FROM `contact` WHERE `dfrn-id` = '%s' LIMIT 1", - dbesc($decrypted_dfrn_id), - intval($local_uid) - ); - if(count($r)) { - $message = t('The ID provided by your system is a duplicate on our system. It should work if you try again.'); - xml_status(1,$message); // Birthday paradox - duplicate dfrn-id - // NOTREACHED - } - - $r = q("UPDATE `contact` SET `dfrn-id` = '%s', `pubkey` = '%s' WHERE `id` = %d LIMIT 1", - dbesc($decrypted_dfrn_id), - dbesc($dfrn_pubkey), - intval($dfrn_record) - ); - if(! count($r)) { - $message = t('Unable to set your contact credentials on our system.'); - xml_status(3,$message); - } - - // We're good but now we have to scrape the profile photo and send notifications. - - require_once("Photo.php"); - - $photo_failure = false; - - $r = q("SELECT `photo` FROM `contact` WHERE `id` = %d LIMIT 1", - intval($dfrn_record)); - if(count($r)) { - - $filename = basename($r[0]['photo']); - $img_str = fetch_url($r[0]['photo'],true); - $img = new Photo($img_str); - if($img->is_valid()) { - - $img->scaleImageSquare(175); - - $hash = photo_new_resource(); - - $r = $img->store($local_uid, $dfrn_record, $hash, $filename, t('Contact Photos') , 4); - - if($r === false) - $photo_failure = true; - - $img->scaleImage(80); - $r = $img->store($local_uid, $dfrn_record, $hash, $filename, t('Contact Photos') , 5); - - if($r === false) - $photo_failure = true; - - $photo = $a->get_baseurl() . '/photo/' . $hash . '-4.jpg'; - $thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.jpg'; - } - else - $photo_failure = true; - } - else - $photo_failure = true; - - if($photo_failure) { - $photo = $a->get_baseurl() . '/images/default-profile.jpg'; - $thumb = $a->get_baseurl() . '/images/default-profile-sm.jpg'; - } - - $new_relation = REL_FAN; - if(($relation == REL_VIP) || ($duplex)) - $new_relation = REL_BUD; - - $r = q("UPDATE `contact` SET - `photo` = '%s', - `thumb` = '%s', - `rel` = %d, - `name-date` = '%s', - `uri-date` = '%s', - `avatar-date` = '%s', - `blocked` = 0, - `pending` = 0, - `duplex` = %d, - `network` = 'dfrn' WHERE `id` = %d LIMIT 1 - ", - dbesc($photo), - dbesc($thumb), - intval($new_relation), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - intval($duplex), - intval($dfrn_record) - ); - if($r === false) { // should not happen unless schema is messed up - $message = t('Unable to update your contact profile details on our system'); - xml_status(3,$message); - } - - // Otherwise everything seems to have worked and we are almost done. Yay! - // Send an email notification + // Main entry point. Our user received a friend request notification (perhaps + // from another site) and clicked 'Accept'. $POST['source_url'] is not set. + // They will perform the following: - $r = q("SELECT * FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid` - WHERE `contact`.`id` = %d LIMIT 1", - intval($dfrn_record) - ); - if((count($r)) && ($r[0]['notify-flags'] & NOTIFY_CONFIRM)) { - - $tpl = (($new_relation == REL_BUD) - ? load_view_file('view/friend_complete_eml.tpl') - : load_view_file('view/intro_complete_eml.tpl')); - - $email_tpl = replace_macros($tpl, array( - '$sitename' => $a->config['sitename'], - '$siteurl' => $a->get_baseurl(), - '$username' => $r[0]['username'], - '$email' => $r[0]['email'], - '$fn' => $r[0]['name'], - '$dfrn_url' => $r[0]['url'], - '$uid' => $newuid ) - ); - - $res = mail($r[0]['email'], t("Connection accepted at ") . $a->config['sitename'], - $email_tpl, 'From: ' . t('Administrator') . '@' . $_SERVER[SERVER_NAME] ); - if(!$res) { - notice( t("Email notification failed.") . EOL ); - } - } - xml_status(0); // Success - return; // NOTREACHED - - ////////////////////// End of this scenario /////////////////////////////////////////////// - } - else { - - // We are processing a local confirmation initiated on this system by our user to an external introduction. + if(! x($_POST,'source_url')) { $uid = get_uid(); @@ -219,18 +19,24 @@ function dfrn_confirm_post(&$a) { notice( t('Permission denied.') . EOL ); return; } + + // These come from the friend request notification form. $dfrn_id = ((x($_POST,'dfrn_id')) ? notags(trim($_POST['dfrn_id'])) : ""); $intro_id = intval($_POST['intro_id']); $duplex = intval($_POST['duplex']); + + // The other person will have been issued an ID when they first requested friendship. + // Locate their record. At this time, their record will have both pending and blocked set to 1. + $r = q("SELECT * FROM `contact` WHERE `issued-id` = '%s' AND `uid` = %d LIMIT 1", dbesc($dfrn_id), intval($uid) ); if(! count($r)) { - notice( t('Node does not exist.') . EOL ); + notice( t('Contact not found.') . EOL ); return; } @@ -240,6 +46,13 @@ function dfrn_confirm_post(&$a) { $dfrn_confirm = $r[0]['confirm']; $aes_allow = $r[0]['aes_allow']; + + // Generate a key pair for all further communications with this person. + // We have a keypair for every contact, and a site key for unknown people. + // This provides a means to carry on relationships with other people if + // any single key is compromised. It is a robust key. We're much more + // worried about key leakage than anybody cracking it. + $res = openssl_pkey_new(array( 'digest_alg' => 'whirlpool', 'private_key_bits' => 4096, @@ -254,6 +67,8 @@ function dfrn_confirm_post(&$a) { $pubkey = openssl_pkey_get_details($res); $public_key = $pubkey["key"]; + // Save the private key. Send them the public key. + $r = q("UPDATE `contact` SET `prvkey` = '%s' WHERE `id` = %d AND `uid` = %d LIMIT 1", dbesc($private_key), intval($contact_id), @@ -263,6 +78,21 @@ function dfrn_confirm_post(&$a) { $params = array(); + // Per the protocol document, we will verify both ends by encrypting the dfrn_id with our + // site private key (person on the other end can decrypt it with our site public key). + // Then encrypt our profile URL with the other person's site public key. They can decrypt + // it with their site private key. If the decryption on the other end fails for either + // item, it indicates tampering or key failure on at least one site and we will not be + // able to provide a secure communication pathway. + + // If other site is willing to accept full encryption, (aes_allow is 1 AND we have php5.3 + // or later) then we encrypt the personal public key we send them using AES-256-CBC and a + // random key which is encrypted with their site public key. + + // Note: We can send any of these things as binary blobs because they are being POST'ed. + // Any protocol conversations (notify, poll) which perform GET require bin2hex of all the + // binary stuff. + $src_aes_key = random_string(); $result = ''; @@ -285,10 +115,12 @@ function dfrn_confirm_post(&$a) { if($duplex == 1) $params['duplex'] = 1; + // POST all this stuff to the other site. + $res = post_url($dfrn_confirm,$params); - // Try to be robust if the remote site is having difficulty and throwing up - // errors of some kind. + // Now figure out what they responded. Try to be robust if the remote site is + // having difficulty and throwing up errors of some kind. $leading_junk = substr($res,0,strpos($res,'status; - $message = unxmlify($xml->message); + $message = unxmlify($xml->message); // human readable text of what may have gone wrong. switch($status) { case 0: notice( t("Confirmation completed successfully.") . EOL); @@ -346,7 +178,7 @@ function dfrn_confirm_post(&$a) { if(($status == 0) && ($intro_id)) { - //delete the notification + // Success. Delete the notification. $r = q("DELETE FROM `intro` WHERE `id` = %d AND `uid` = %d LIMIT 1", intval($intro_id), @@ -358,6 +190,10 @@ function dfrn_confirm_post(&$a) { if($status != 0) return; + // We have now established a relationship with the other site. + // Let's make our own personal copy of their profile photo so we don't have + // to always load it from their site. + require_once("Photo.php"); $photo_failure = false; @@ -428,8 +264,231 @@ function dfrn_confirm_post(&$a) { if($r === false) notice( t('Unable to set contact photo.') . EOL); + + // Let's send our user to the contact editor in case they want to + // do anything special with this new friend. + goaway($a->get_baseurl() . '/contacts/' . intval($contact_id)); return; //NOTREACHED } - return; + + + + // End of first scenario. [Local confirmation of remote friend request]. + + + + // Begin scenario two. This is the remote response to the above scenario. + // This will take place on the site that originally initiated the friend request. + // In the section above where the confirming party makes a POST and + // retrieves xml status information, they are communicating with the following code. + + if(x($_POST,'source_url')) { + + // We are processing an external confirmation to an introduction created by our user. + + $public_key = $_POST['public_key']; + $dfrn_id = $_POST['dfrn_id']; + $source_url = $_POST['source_url']; + $aes_key = $_POST['aes_key']; + $duplex = $_POST['duplex']; + $version_id = $_POST['dfrn_version']; + + // Find our user's account + + $r = q("SELECT * FROM `user` WHERE `nickname` = '%s' LIMIT 1", + dbesc($node)); + + if(! count($r)) { + $message = t('No user record found for ') . '\'' . $node . '\''; + xml_status(3,$message); // failure + // NOTREACHED + } + + $my_prvkey = $r[0]['prvkey']; + $local_uid = $r[0]['uid']; + + + if(! strstr($my_prvkey,'BEGIN RSA PRIVATE KEY')) { + $message = t('Our site encryption key is apparently messed up.'); + xml_status(3,$message); + } + + // verify everything + + $decrypted_source_url = ""; + openssl_private_decrypt($source_url,$decrypted_source_url,$my_prvkey); + + + if(! strlen($decrypted_source_url)) { + $message = t('Empty site URL was provided or URL could not be decrypted by us.'); + xml_status(3,$message); + // NOTREACHED + } + + $ret = q("SELECT * FROM `contact` WHERE `url` = '%s' AND `uid` = %d LIMIT 1", + dbesc($decrypted_source_url), + intval($local_uid) + ); + + if(! count($ret)) { + // this is either a bogus confirmation (?) or we deleted the original introduction. + $message = t('Contact record was not found for you on our site.'); + xml_status(3,$message); + return; // NOTREACHED + } + + $relation = $ret[0]['rel']; + + // Decrypt all this stuff we just received + + $foreign_pubkey = $ret[0]['site-pubkey']; + $dfrn_record = $ret[0]['id']; + + $decrypted_dfrn_id = ""; + openssl_public_decrypt($dfrn_id,$decrypted_dfrn_id,$foreign_pubkey); + + if(strlen($aes_key)) { + $decrypted_aes_key = ""; + openssl_private_decrypt($aes_key,$decrypted_aes_key,$my_prvkey); + $dfrn_pubkey = openssl_decrypt($public_key,'AES-256-CBC',$decrypted_aes_key); + } + else { + $dfrn_pubkey = $public_key; + } + + $r = q("SELECT * FROM `contact` WHERE `dfrn-id` = '%s' LIMIT 1", + dbesc($decrypted_dfrn_id), + intval($local_uid) + ); + if(count($r)) { + $message = t('The ID provided by your system is a duplicate on our system. It should work if you try again.'); + xml_status(1,$message); // Birthday paradox - duplicate dfrn-id + // NOTREACHED + } + + $r = q("UPDATE `contact` SET `dfrn-id` = '%s', `pubkey` = '%s' WHERE `id` = %d LIMIT 1", + dbesc($decrypted_dfrn_id), + dbesc($dfrn_pubkey), + intval($dfrn_record) + ); + if(! count($r)) { + $message = t('Unable to set your contact credentials on our system.'); + xml_status(3,$message); + } + + // We're good but now we have to scrape the profile photo and send notifications. + + require_once("Photo.php"); + + $photo_failure = false; + + $r = q("SELECT `photo` FROM `contact` WHERE `id` = %d LIMIT 1", + intval($dfrn_record)); + if(count($r)) { + + $filename = basename($r[0]['photo']); + $img_str = fetch_url($r[0]['photo'],true); + $img = new Photo($img_str); + if($img->is_valid()) { + + $img->scaleImageSquare(175); + + $hash = photo_new_resource(); + + $r = $img->store($local_uid, $dfrn_record, $hash, $filename, t('Contact Photos') , 4); + + if($r === false) + $photo_failure = true; + + $img->scaleImage(80); + $r = $img->store($local_uid, $dfrn_record, $hash, $filename, t('Contact Photos') , 5); + + if($r === false) + $photo_failure = true; + + $photo = $a->get_baseurl() . '/photo/' . $hash . '-4.jpg'; + $thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.jpg'; + } + else + $photo_failure = true; + } + else + $photo_failure = true; + + if($photo_failure) { + $photo = $a->get_baseurl() . '/images/default-profile.jpg'; + $thumb = $a->get_baseurl() . '/images/default-profile-sm.jpg'; + } + + $new_relation = REL_FAN; + if(($relation == REL_VIP) || ($duplex)) + $new_relation = REL_BUD; + + $r = q("UPDATE `contact` SET + `photo` = '%s', + `thumb` = '%s', + `rel` = %d, + `name-date` = '%s', + `uri-date` = '%s', + `avatar-date` = '%s', + `blocked` = 0, + `pending` = 0, + `duplex` = %d, + `network` = 'dfrn' WHERE `id` = %d LIMIT 1 + ", + dbesc($photo), + dbesc($thumb), + intval($new_relation), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + intval($duplex), + intval($dfrn_record) + ); + if($r === false) { // indicates schema is messed up or total db failure + $message = t('Unable to update your contact profile details on our system'); + xml_status(3,$message); + } + + // Otherwise everything seems to have worked and we are almost done. Yay! + // Send an email notification + + $r = q("SELECT * FROM `contact` LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid` + WHERE `contact`.`id` = %d LIMIT 1", + intval($dfrn_record) + ); + if((count($r)) && ($r[0]['notify-flags'] & NOTIFY_CONFIRM)) { + + $tpl = (($new_relation == REL_BUD) + ? load_view_file('view/friend_complete_eml.tpl') + : load_view_file('view/intro_complete_eml.tpl')); + + $email_tpl = replace_macros($tpl, array( + '$sitename' => $a->config['sitename'], + '$siteurl' => $a->get_baseurl(), + '$username' => $r[0]['username'], + '$email' => $r[0]['email'], + '$fn' => $r[0]['name'], + '$dfrn_url' => $r[0]['url'], + '$uid' => $newuid ) + ); + + $res = mail($r[0]['email'], t("Connection accepted at ") . $a->config['sitename'], + $email_tpl, 'From: ' . t('Administrator') . '@' . $_SERVER[SERVER_NAME] ); + if(!$res) { + notice( t("Email notification failed.") . EOL ); + } + } + xml_status(0); // Success + return; // NOTREACHED + + ////////////////////// End of this scenario /////////////////////////////////////////////// + } + + // somebody arrived here by mistake or they are fishing. Send them to the homepage. + + goaway($a->get_baseurl()); + // NOTREACHED + } -- 2.39.2