openmsg.io
Initial Handshake
See first: Setup: Guide
See also: Handshake: Reference


The initial handshakes establishes a trust between the Sending Node and the Receiving Node/User. This proves that the Sending Node has permission to send messages to the User. This only needs to be done once.

For a succesfull handshake the Sending Node needs to know the openmsg_address they will be sending messages to, but they will also need to be given a pass_code by the User.

Step 1
The User asks the Receiving Node to generated this pass_code. This pass_code will have a short lifespan (1 hour or less) the Receiving Node stores this pass_code against the User's openmsg_address for later verification.

The User then passes the pass_code to the Sending Node along with their openmsg_address, and the Sending Node passes a from_openmsg_address and also the pass_code and the openmsg_address that they want to connect to, back to the Receiving Node to prove that they are authorized to send messages to the User. The pass_code is one-use only and has a limited lifespan.

Step 2a
The Receiving Node verifies that the pass_code is valid for this given openmsg_address. The Receiving Node then sends a curl request to the domain in the from_openmsg_address, passing the openmsg_address and pass_code back so the domain can confirm it made that request.

Step 2b
The Receiving Node should mark the pass_code as used at this point so it cant be used again, then it should generate and sends an auth_code and ident_code to the Sending Node which the Sending Node will use in future to authenticate themselves and send messages to the User. The auth_code and ident_code are long-lasting and re-usable. Both Sending Node and Receiving Node should store these for future use.

By default, authorizations are two-way. For one-way messages, the Sending Node doesn't need to accept messages back. However if the Sending Node wants to authorize replies, the Sending Node also need to set up as a Receiving Node to enable two-way communication, and they should set allow_replies to 'true' in the json data below. The Sending Node should always send thier own openmsg_address (as from_openmsg_address) whether they accept replies or not. Once the Receiving Node has sent the auth_code, ident_code and message_crypt_key back to the Sending Node, they will both store and use these for both sending and receiving messages.
Sending Component
Request Authorization
For a Sending Node to request authorization from the Receiving Node, the User will first generate a one-use pass_code from their Openmsg account Receiving Node. This will be passed on to the Sending Node (before it expires) along with the Openmsg address probably via a form (see exmaple page1.php below).
HTML
handshake-form.php
<form id="openmsg_form" name="openmsg_form" method="post" action="initiate-handshake.php">
   <input type="text" name="other_openmsg_address" id="other_openmsg_address" />
   <input type="password" name="pass_code" id="pass_code" />
   <input type="submit" name="button" id="button" value="Submit" />
</form>
Once the Sending Node has the openmsg_address and the one-use pass_code, it uses these to request authorization from the Receiving Node. (The Sending Node will also send a from_openmsg_address_name and from_openmsg_address to the Receiving Node). If successful, the Receiving Node will respond with an auth_code, an ident_code and a message_crypt_key which the Sending Node needs to store. The Sending Node will used these in future to authenticate themselves with the Receiving Node when sending messages to the User.
PHP
initiate-handshake.php
<?php
// ##### See: openmsg.io/docs/v1 

// To Do: !IMPORTANT - This file should be stored somewhere password protected so only logged in users can access it.

require "../openmsg_settings.php";

// Variables from an input form, filled in by an Openmsg User
$other_openmsg_address = $_POST["other_openmsg_address"]; 
$pass_code = $_POST["pass_code"]; 

//To Do: Get $self_openmsg_address, $self_openmsg_address_name, $self_allow_replies from database / logged in user
$self_openmsg_address_name = "John Doe"; // To Do: replace with the name of the person who owns self_openmsg_address, to help the User identify them. This should be set even if replies are not allowed
$self_openmsg_address = "1000001*".$my_openmsg_domain; // To Do: replace with the from/reply address. This should be set even if replies are not allowed
$self_allow_replies = true; // This tells the Receiving Node if replies can be received.

function initiate_handshake($db, $other_openmsg_address, $pass_code, $self_openmsg_address, $self_openmsg_address_name, $self_allow_replies, $my_openmsg_domain, $sandbox_dir){
    
    if(!$other_openmsg_address || !$pass_code || !$self_openmsg_address || !$self_openmsg_address_name || !$my_openmsg_domain) { 
        // If the database doesnt contain an ident_code / openmsg_address_id combo
          // or there are other errors then return error and error_message
        $response = array("error"=>TRUE, "response_code"=>"SM_E000", "error_message"=>"Missing data (8BgrT)"); 
        return($response);
    }
    
    // Check the data is formatted correctly
    // Check that openmsg_address_id is numeric only
    
    if(ctype_digit((string)$pass_code) == false) {
        //return("Please enter a Pass Code");
    }
    
    // Split the Openmsg address into the address ID and the domain 
    $other_openmsg_address_id = explode("*", $other_openmsg_address)[0]; 
    $other_openmsg_address_domain = explode("*", $other_openmsg_address)[1]; 
    
    // Check that openmsg_address_id is numeric only
    if(ctype_digit((string)$other_openmsg_address_id) == false) {
        return("other_openmsg_address_id not valid $other_openmsg_address_i (wD861)");
    }
    
    // Check that openmsg_address_domain only contains valid characters
    if(preg_match("/[^A-Za-z0-9.\-]/", $other_openmsg_address_domain) == true) {
        return("openmsg_address_domain not valid $other_openmsg_address_domain (D8hgB)");
    }
    
    // Store other_openmsg_address and pass_code temporarily in a separate table "openmsg_handshakes" with a short expiry timestamp (60 seconds)
      // to make note of a pending Authorization Request as it will be used by the Receiving Node to confirm that the request
      // originated from the same domain as the domain in other_openmsg_address.
    $stmt = $db->prepare("INSERT INTO openmsg_handshakes (other_openmsg_address, pass_code) VALUES (?, ?)");
    $stmt->bind_param("ss", $other_openmsg_address, $pass_code);
    $stmt->execute(); // To Do: Un-comment
    $stmt->close();
    

    
    // ##### Start CURL request ##### 
    // The url is created from the domain in the Openmsg address 
    $url = "https://".$other_openmsg_address_domain."/openmsg".$sandbox_dir."/auth/"; 

    // JSON data
    $data = 
        array(
          "receiving_openmsg_address_id" => $other_openmsg_address_id,
          "pass_code" => $pass_code,
          "sending_openmsg_address" => $self_openmsg_address,
          "sending_openmsg_address_name" => $self_openmsg_address_name,
          "sending_allow_replies" => $self_allow_replies,
          "openmsg_version" => 1.0
       );
    $data = json_encode($data);
    
    
    $curl = curl_init();  
    curl_setopt($curl, CURLOPT_URL, $url); 
    curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
    curl_setopt($curl, CURLOPT_POST, true); 
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false); 
    curl_setopt($curl, CURLOPT_MAXREDIRS, 3); 
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data); 

    
    $response = curl_exec($curl);
    
    $info = curl_getinfo($curl);
    if ($info["http_code"] == 301 || $info["http_code"] == 302) {
        $redirectUrl = $info["redirect_url"]; // Not always present; you may need to parse headers manually
        curl_setopt($curl, CURLOPT_URL, $redirectUrl);
        $response = curl_exec($curl);
    }
    
    $response = json_decode($response, true);
    curl_close($curl); 
    
    // Check for errors
    if($err = curl_error($curl)) return("Error. Curl error: ".$err);
    if(($curl_status = curl_getinfo($curl, CURLINFO_HTTP_CODE)) != 200) return("Error. Curl response status: ".$curl_status." (A50m5)");
    if($response["error"]) return("Error: ".$response["error_message"]);
    if(($success = $response["success"]) != TRUE) return("Error: Unsuccessful from initiate-handshake.php (LyoSV) ");
    
    if(!($auth_code = $response["auth_code"])) return("Error: Missing auth_code in response (CxMBc)");
    if(!($ident_code = $response["ident_code"])) return("Error: Missing ident_code in response (BC2Lm)");
    if(!($message_crypt_key = $response["message_crypt_key"])) return("Error: Missing message_crypt_key in response (nEG5M)");
    if(!($other_openmsg_address_name = $response["self_openmsg_address_name"])) return("Error: Missing openmsg_address_name in response (92Apb)");
    
    $other_acceptsMessages = TRUE;
    
    // Delete any previous connections between these two users so they only have one connection at a time.
    $stmt = $db->prepare("DELETE FROM openmsg_user_connections WHERE self_openmsg_address = ? AND other_openmsg_address = ?");
    $stmt->bind_param("ss", $self_openmsg_address, $other_openmsg_address);
    $stmt->execute(); // To Do: Un-comment
    $stmt->close();    
    
     // Store the openmsg_address, message_crypt_key, auth_code and ident_code in the database as they will be the 
         // codes to send messages to the user and to authenticate messages from the User
    $stmt = $db->prepare("INSERT INTO openmsg_user_connections (self_openmsg_address, other_openmsg_address, other_openmsg_address_name, other_acceptsMessages, auth_code, ident_code, message_crypt_key) 
                                                                  VALUES (?, ?, ?, ?, ?, ?, ?)");
    $stmt->bind_param("sssssss", $self_openmsg_address, $other_openmsg_address, $other_openmsg_address_name, $other_acceptsMessages, $auth_code, $ident_code, $message_crypt_key);
    $stmt->execute(); // To Do: Un-comment
    $stmt->close();
    return("Success");
    
}
$response = initiate_handshake($db, $other_openmsg_address, $pass_code, $self_openmsg_address, $self_openmsg_address_name, $self_allow_replies, $my_openmsg_domain, $sandbox_dir);
if($response == "Success"){
    echo "Connected. You can now message us: ".$self_openmsg_address;
    //echo "Connected. You are now subsribed for updates about your order."; // Show an appropriate response
}else{
    echo "Error: ".$response;
}
?>
When the Receiving Node receives the openmsg_address_id and pass_code from the Sending Node, it will make a cURL post back to the Sending Node to the domain from from_openmsg_address. The Sending Node will check its database for a pending authorization with openmsg_address and pass_code and confirm that the request did originate from them, and check that the handshake has not expired (60 second expiry window for in-progress pending handshakes).
  • That the account for openmsg_address_id is an actual account
  • That the pass_code was generated by the User: openmsg_address_id, is genuine, has not been used
  • And that it has not expired (1 hour expiry window for pass_codes)
(Receiving Node: see the Guide and Reference for generating a pass_code)

If openmsg_address_id and pass_code pass the tests then the Receiving Node will generate an auth_code and an ident_code. Both Nodes will need to store these values so that the Sending Node can send messages to the User and so that the Receiving Node can authenicate the sender.
PHP
/openmsg/auth-confirm
<?php
// ##### See: openmsg.io/docs/v1

require "../openmsg_settings.php";

$data = json_decode(file_get_contents("php://input"), true);

$other_openmsg_address = $data["other_openmsg_address"]; 
$pass_code = $data["pass_code"]; 


function auth_confirm ($db, $other_openmsg_address, $pass_code){
    // Query database to check for a pending authorization request with openmsg_address and pass_code 
    $stmt = $db->prepare("SELECT UNIX_TIMESTAMP(timestamp) FROM openmsg_handshakes WHERE other_openmsg_address = ? AND pass_code = ?");
    $stmt->bind_param("ss", $other_openmsg_address, $pass_code);
    $stmt->execute();  // To Do: Un-comment
    $stmt->store_result();
    $stmt->bind_result($initation_timestamp);
    $stmt->fetch();
    $matching_handshakes = $stmt->num_rows;
    $stmt->close();
    
    if($matching_handshakes == 0) { 
        // If the pass_code isnt present for the openmsg_address then return error and error_message
        $response = array("error"=>TRUE, "error_message"=>"unknown pending authorization with $other_openmsg_address, $pass_code"); 
        return($response);
    }
    if($initation_timestamp < time()-60){
        // If the pass_code has expired then return error and error_message
        $response = array("error"=>TRUE, "error_message"=>"expired handshake, over 60 seconds old"); 
        return($response);
    }
    
    // The pending handshake has been verified once. It should now be deleted at this stage so it cant be re-used
    $stmt = $db->prepare("DELETE FROM openmsg_handshakes WHERE other_openmsg_address = ? AND pass_code = ? LIMIT 1");
    $stmt->bind_param("ss", $other_openmsg_address, $pass_code);
    $stmt->execute(); // To Do: Un-comment
     
    $response = array("success"=>TRUE); 
    return($response);
}

$response = auth_confirm ($db, $other_openmsg_address, $pass_code); // $db = database connection
echo json_encode($response);   

?>
Receiving Component
Authorize Sender
The Receiving Node will then check openmsg_address_id and pass_code to make sure:
-that the account for openmsg_address_id is an actual account on their server
-that the pass_code was generated by the User: pass_code, is genuine, has not been used, and has not expired
(Receiving Node: see the Guide and Reference for generating a pass_code)

If openmsg_address_id and pass_code pass the tests then the Receiving Node will generate an auth_code, an ident_code and message_crypt_key. Both Nodes will need to store these values so that the Sending Node can send encrypted messages to the User and so that the Receiving Node can authenicate the sender and decrypt messages.
PHP
/openmsg/auth
<?php
// ##### See: openmsg.io/docs/v1

require "../openmsg_settings.php";

// To Do: $my_openmsg_domain needs to be filled in with your actual domain name in the openmsg_settings.php file

$data = json_decode(file_get_contents("php://input"), true);

$self_openmsg_address_id = $data["receiving_openmsg_address_id"]; 
$pass_code = $data["pass_code"]; 
$other_openmsg_address = $data["sending_openmsg_address"]; 
$other_openmsg_address_name = $data["sending_openmsg_address_name"]; 
$other_allows_replies = $data["sending_allow_replies"];



function auth_check ($db, $self_openmsg_address_id, $pass_code, $other_openmsg_address, $other_openmsg_address_name, $other_allows_replies, $my_openmsg_domain, $sandbox_dir){
    // Verify the data received is present, else return an error
    if($self_openmsg_address_id == "" || $pass_code == "" || $other_openmsg_address == "" || $other_openmsg_address_name == "") {
        $response = array("error"=>TRUE, "error_message"=>"self_openmsg_address_id, pass_code, other_openmsg_address and other_openmsg_address_name cannot be blank"); 
        return($response);
    }
    
    $self_openmsg_address = $self_openmsg_address_id."*".$my_openmsg_domain;
    
    $stmt = $db->prepare("SELECT self_openmsg_address_name FROM openmsg_users WHERE self_openmsg_address = ?");
    $stmt->bind_param("s", $self_openmsg_address);
    $stmt->execute(); // To Do: Un-comment
    $stmt->store_result();
    $stmt->bind_result($self_openmsg_address_name);
    $stmt->fetch();
    $matching_passCodes = $stmt->num_rows;
    $stmt->close();
    
    // To Do: Query database to check validity of pass_code / $openmsg_address_id combo
    $stmt = $db->prepare("SELECT UNIX_TIMESTAMP(timestamp) FROM openmsg_passCodes WHERE self_openmsg_address = ? AND pass_code = ?");
    $stmt->bind_param("ss", $self_openmsg_address, $pass_code);
    $stmt->execute(); // To Do: Un-comment
    $stmt->store_result();
    $stmt->bind_result($passCode_timestamp);
    $stmt->fetch();
    $matching_passCodes = $stmt->num_rows;
    $stmt->close();
    
    if($matching_passCodes == 0){
        // If there are no matching pass_codes for that address then return error and error_message
        $response = array("error"=>TRUE, "error_message"=>"pass code not valid"); 
        return($response); 
    }
    
    $oneHour = 3600; // 3600 seconds
    if($passCode_timestamp < time()-$oneHour){
        // If the pass_code has expired then return error and error_message
        $response = array("error"=>TRUE, "error_message"=>"expired pass code, over 1 hour old"); 
        return($response); 
    }
    
    // The pass code has been verified once. It should now be deleted at this stage so it cant be re-used
    $stmt = $db->prepare("DELETE FROM openmsg_passCodes WHERE self_openmsg_address = ? AND pass_code = ? LIMIT 1");
    $stmt->bind_param("ss", $self_openmsg_address, $pass_code);
    $stmt->execute(); // To Do: Un-comment
    
    // Split the other_openmsg_address into the address ID and the sending domain 
    $other_openmsg_address_id = explode("*", $other_openmsg_address)[0]; 
    $other_openmsg_address_domain = explode("*", $other_openmsg_address)[1]; 
    
    
    // Check that openmsg_address_id is numeric only
    if(ctype_digit($other_openmsg_address_id) == false) {
        $response = array("error"=>TRUE, "error_message"=>"other_openmsg_address_id should be numeric"); 
        return($response);
    }
    
    // ##### Start CURL request #####
    // The url is created from the domain in the initiating Openmsg address 
    // The domain checks its temporary records for a pending authorization with openmsg_address and pass_code
    // This confirms that the request has come from the domain in the other_openmsg_address
    $url = "https://".$other_openmsg_address_domain."/openmsg".$sandbox_dir."/auth-confirm/"; 
    
    // JSON data
    $data = 
        array(
          "other_openmsg_address" => $self_openmsg_address_id."*".$my_openmsg_domain, 
          "pass_code" => $pass_code
       );
    $data = json_encode($data);
    
    
    $curl = curl_init();  
    curl_setopt($curl, CURLOPT_URL, $url); 
    curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
    curl_setopt($curl, CURLOPT_POST, true); 
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false); 
    curl_setopt($curl, CURLOPT_MAXREDIRS, 3); 
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data); 
    

    
    $response = curl_exec($curl);
    
    $info = curl_getinfo($curl);
    if ($info["http_code"] == 301 || $info["http_code"] == 302) {
        $redirectUrl = $info["redirect_url"]; // Not always present; you may need to parse headers manually
        curl_setopt($curl, CURLOPT_URL, $redirectUrl);
        $response = curl_exec($curl);
        //return(array("error"=>TRUE, "error_message"=>"Error: Redirect URL ".$redirectUrl));
    }
    
    $response = json_decode($response, true);
    curl_close($curl);  
    
    // Check for errors
    if($err = curl_error($curl)) return(array("error"=>TRUE, "error_message"=>"Error. Curl error: ".$err));
    if(($curl_status = curl_getinfo($curl, CURLINFO_HTTP_CODE)) != 200) return(array("error"=>TRUE, "error_message"=>"Error. Curl response status -- : ".$curl_status));
    if($response["error"]) return(array("error"=>TRUE, "error_message"=>"Error: ".$response["error_message"]));
    if(($success = $response["success"]) != TRUE) return(array("error"=>TRUE, "error_message"=>"Error: Unsuccessful from /auth/"));
    
    
    if(!$error){
        
        $auth_code = bin2hex(random_bytes(32));  
        $ident_code = bin2hex(random_bytes(32)); 
        $message_crypt_key = sodium_bin2hex(sodium_crypto_secretbox_keygen());
        
        // Delete any previous connections between these two users so they only have one connection at a time.
        $stmt = $db->prepare("DELETE FROM openmsg_user_connections WHERE self_openmsg_address = ? AND other_openmsg_address = ?");
        $stmt->bind_param("ss", $self_openmsg_address, $other_openmsg_address);
        $stmt->execute(); // To Do: Un-comment
        $stmt->close();    
        
        // Store the auth_code, ident_code, message_crypt_key, other_openmsg_address, other_openmsg_address_name in your 
            // database table "openmsg_user_connections" so that the two accounts can now communicate unsing the auth_code etc.
        $stmt = $db->prepare("INSERT INTO openmsg_user_connections (self_openmsg_address, other_openmsg_address, other_openmsg_address_name, other_acceptsMessages, auth_code, ident_code, message_crypt_key) 
                                                                  VALUES (?, ?, ?, ?, ?, ?, ?)");
        $stmt->bind_param("sssssss", $self_openmsg_address, $other_openmsg_address, $other_openmsg_address_name, $other_allows_replies, $auth_code, $ident_code, $message_crypt_key);
        $stmt->execute(); // To Do: Un-comment
        $stmt->close();    
        
        // To Do: Receiving Node: IMPORTANT - Mark the pass_code as used in the database table "openmsg_passCodes" so it cant be used again. 
        $stmt = $db->prepare("DELETE FROM openmsg_passCodes WHERE self_openmsg_address = ? AND pass_code = ? LIMIT 1");
        $stmt->bind_param("ss", $self_openmsg_address, $pass_code);
        $stmt->execute(); // To Do: Un-comment    
        
        
        $response = array("success"=>TRUE, "auth_code"=>$auth_code, "ident_code"=>$ident_code, "message_crypt_key"=>$message_crypt_key, "other_openmsg_address_name"=>$self_openmsg_address_name); 
        
        return($response);
    }else{
        $response = array("error"=>TRUE, "error_message"=>"Error"); 
        return($response);
    }
}

$response = auth_check ($db, $self_openmsg_address_id, $pass_code, $other_openmsg_address, $other_openmsg_address_name, $other_allows_replies, $my_openmsg_domain, $sandbox_dir); // $db = database connection
echo json_encode($response);  

?>
One-way or Two-way Messaging
One-way
The Sending Node can send messages to the User via their Receiving Node. The Sending Node does not have to be set up to be able to receive messages. This is indicated by allow_replies. Whether the Sending Node allows replies or not should be stored by the Receiving Node and be handled by the Receiving Node accordingly.
Two-way
Since the message_crypt_key, auth_code and ident_code are stored by the Receiving Node and Sending Node in their database as the codes to prove authentication of an outgoing message AND as the codes to authenticate a received message, then two way messaging is available by default and no extra code or steps are needed at this point other than setting allow_replies to 'true'.
!Important
The Receiving Node MUST receive a valid pass_code to authorize the sender. This is core to Openmsg.
To finish
Recap
  • The User requests a pass_code from their Receiving Node (the provider of their Openmsg account)
  • The Receiving Node generates this and stores it with a 1 hour expiry timestamp, associated with the User
  • The User types their pass_code and their openmsg_address into a web form operated by the Sending Node
  • The Sending Node sends the User's openmsg_address_id and pass_code to the User's Receiving Node. A valid pass_code proves that the handshake request came from the User. (The Sending Node also sends their from_openmsg_address and from_openmsg_address_name)
  • Before the The Receiving Node confirms anything, it sends a request back to the domain in the from_openmsg_address to varify request did come from the domain
  • The Sending Node checks that the request did come from them and confirms
  • Upon confirmation, the Receiving Node checks their database for the pass_code assosiated with the User's openmsg_address. If valid it generates, stores and sends a message_crypt_key, auth_code and ident_code and also sends the User's openmsg_address_name
  • The Sending Node receives the message_crypt_key, auth_code, ident_code and the User's openmsg_address_name and stores these permanently
  • The Handshake is complete and the two Nodes can send messages between these connected Users
Next