phpencryptionpgplibsodiumsecure-coding

Should both the sender's keypair and the recipient's keypair open a sodium crypto box?


I'm trying to understand sharing encrypted messages using PHP's sodium crypto box. Using an example that I found somewhere I've tried to simulate a conversation between two people. They each have created their own Public/Private keyset on their local system.

The sender creates a shared key using:

sodium_crypto_box_keypair_from_secretkey_and_publickey(Sender's Private Key, Recipient's Public Key) 

The sender then packages the messsage using:

sodium_crypto_box($message, nonce, Sender's keypair)

From what I've read this signs the message for authentication and then encrypt's the message for secure transport. At the receiving end the recipient creates his own keyset using his private key and the sender's public key. He then opens the box using his local keyset.

sodium_crypto_box_open($encrypted_signed_text, nonce, Recipient's keypair)

This all looked fine. Secure communications were made possible. Or was it?

By a cut-and-paste error I discovered that the sender could also decrypt the encrypted message.

sodium_crypto_box_open($encrypted_signed_text, nonce, Sender's keypair)

How is that possible without access to the recipient's private key?

What's really going on behind the curtain? Am I using this function properly? Everything I've read says we shouldn't roll our own crypto functions because they have not been mathmatically proven to be crypto safe. Is this process crypto safe? Has the building of this keyset been proven mathmatically by those in the crypto community that actually know what they're doing rather than someone like me that just kludges together someone else's work without truly understanding it? Granted, what I'll be communicating won't be worth anyone's time to crack but for the priciple of it I would still like to do it right.

MY Code:

    
    <?php
    
    //===============================================================
    
    //This is another Puplic Key Example with sender authentication
    
    // On Alice's device
    $alice_keypair = sodium_crypto_box_keypair();
    $alice_secret_key = sodium_crypto_box_secretkey($alice_keypair);
    $alice_public_key = sodium_crypto_box_publickey($alice_keypair);
    
    // On Bob's device
    $bob_keypair = sodium_crypto_box_keypair();
    $bob_secret_key = sodium_crypto_box_secretkey($bob_keypair);
    $bob_public_key = sodium_crypto_box_publickey($bob_keypair);
    
    // Exchange keys:
    // - Send Alice's public key to Bob.
    // - Send Bob's public key to Alice.
    
    // On sender:
    
    // Create nonce. Convert to Hex so it may be easily stored and transmitted
    $hexnonce = sodium_bin2hex(random_bytes(\SODIUM_CRYPTO_BOX_NONCEBYTES));
    
    // Create enc/sign key pair.
    $sender_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($alice_secret_key, $bob_public_key);

    $message = "New message: Hi Bob, I'm Alice";

    // Encrypt and sign the message with the sender's keyset
    //Here I assume the function is using the sender's private key portion to sign the message
    //and using the recipient's public key portion to encrypt the message
    $hexencrypted_signed_text = sodium_bin2hex(sodium_crypto_box($message, sodium_hex2bin($hexnonce), $sender_keypair));

    //Here the message and nonce are transmitted to the recipient...

    // On recipient:
    $recipient_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($bob_secret_key, $alice_public_key);
    
    // Authenticate and decrypt message
    //Here I assume the function is using the Recipient's private key portion of the keyset to decrypt the message
    //and using the sender's public key portion to authenticate that the message was signed by Alice.
    $NewOrigMsg = sodium_crypto_box_open(sodium_hex2bin($hexencrypted_signed_text), sodium_hex2bin($hexnonce), $recipient_keypair);

    echo "<br>===The Original message===<br>";
    var_dump($message); // "Hi Bob, I'm Alice"
    
    echo "<br><br>===Transmitted Values===<br>";
    echo "Nonce: " . $hexnonce . "<br>";
    echo "Encrypted Message: " . $hexencrypted_signed_text . "<br>";
    
    echo "<br>=== Recipient's Decrypted Version===<br>";
    var_dump($NewOrigMsg); // "Hi Bob, I'm Alice"
    
    //Notice that the sender can decrypt the message using the original encryption keypair
    //I would expect that this would not be possible since the sender does not have access to the Recipient's private key
    $orig_msg = sodium_crypto_box_open(sodium_hex2bin($hexencrypted_signed_text), sodium_hex2bin($hexnonce), $sender_keypair);
    echo "<br><br>===Senders's Decrypted Version===<br>";
    var_dump($orig_msg); // "Hi Bob, I'm Alice"
    //==============================================================
    
    ?>
    

I tried to verify my understanding of the example I found on line by building a page with my own tweaks for my current project.


Solution

  • As you yourself have already recognized in your comment, you have misunderstood the crypto_box concept: On the sender side, neither is the recipient's public key used for encryption and the sender's secret key used for signing, nor on the recipient side is the recipient's secret key used for decryption and the sender's public key used for verification.

    crypto_box is a public-key authenticated encryption. It uses X25519 for key agreement and XSalsa20-Poly1305 for authenticated encryption.
    For encryption, the combination K(secretS, publicR) consisting of the sender's secret key and the recipient's public key is used to derive a shared secret using X25519.
    Likewise, on the receiver side, the combination K(secretR, publicS) consisting of the receiver's secret key and the sender's public key is used during decryption to derive a shared secret using X25519.
    The X25519 algorithm ensures that both shared secrets generated in this way are identical.

    Libsodium provides the method sodium_crypto_scalarmult() for X25519 to generate a shared secret, which can be used to demonstrate this:

    <?php
    $sender_keypair = sodium_crypto_box_keypair();
    $sender_secret_key = sodium_crypto_box_secretkey($sender_keypair);
    $sender_public_key = sodium_crypto_box_publickey($sender_keypair);  
    
    $receiver_keypair = sodium_crypto_box_keypair();
    $receiver_secret_key = sodium_crypto_box_secretkey($receiver_keypair);
    $receiver_public_key = sodium_crypto_box_publickey($receiver_keypair);
    
    $sender_shared_secret = sodium_crypto_scalarmult($sender_secret_key, $receiver_public_key);
    $receiver_shared_secret = sodium_crypto_scalarmult($receiver_secret_key, $sender_public_key);
    
    print(strcmp($sender_shared_secret, $receiver_shared_secret) == 0 ? "match" : "no match"); // match
    ?>
    

    After both sides have generated an identical shared secret, both sides derive an identical key from it (using HSalsa20), which is used for authenticated encryption with XSalsa20-Poly1305. Note that all the processing described is wrapped in crypto_box (i.e. none of this has to be done by yourself).
    Incidentally, XSalsa20-Poly1305 is also the algorithm used by crypto_secretbox, Libsodium's secret-key authenticated encryption.

    The common crypto_box use case is that the sender encrypts with K(secretS, publicR) and the receiver decrypts with K(secretR, publicS). Of course, decryption with K(secretS, publicR) is also trivially possible, but this is only relevant for the sender side, as only it has the sender's secret key.
    This is what applies to your last decryption: The sender side decrypts with K(secretS, publicR). By the way, this is not a security problem, because it only means that the sender can decrypt its own ciphertext.