paragonie / sodium-plus

Developer-friendly libsodium interface
Other
170 stars 21 forks source link

Encrypt on frontend (sodium-plus.js) with public-key from backend (PHP sodium) #29

Closed weva-io closed 4 years ago

weva-io commented 4 years ago

I originally posted this on S/O yesterday, but figured it is probably more appropiate and straightforward to ask here.

I would like to achieve an anonymous public-key encryption in a web browser using sodium-plus.js with keys generated in PHP sodium like this:

    $keyPair = sodium_crypto_box_keypair();

    $privateKey = sodium_crypto_box_secretkey($keyPair);
    $publicKey = sodium_crypto_box_publickey($keyPair);

The keys generated with this method work fine in PHP with the sodium_crypto_box_seal and sodium_crypto_box_seal_open methods, but however, I am unable to make it work on the frontend. My approach:

    <script type='text/javascript' src='js/sodium-plus.min.js?v=0.4.2'></script>
    <script>
    async function getPublicKey() {

        return X25519PublicKey.from(
            '<?php echo sodium_bin2hex($publicKey); ?>', // 6a00b1550ccdeff3886a469b9cd4e5dc9aecd30f5deb3dd3e29fd01f8a32103f
            'hex'
        );

    }

    async function encryptString(clearText, publicKey) {

        if (!window.sodium) window.sodium = await SodiumPlus.auto();

        let cipherText = await sodium.crypto_box_seal(clearText, publicKey);

        return cipherText.toString('hex');

    }

    (async function () {

        let clearText = "String that contains secret.";
        let publicKey = await getPublicKey();

        console.log(await encryptString(clearText,publicKey));

    })();
    </script>

This returns TypeError: Argument 2 must be an instance of X25519PublicKey in the console.

Notes:

  1. A public-key that is derived from sodium.crypto_box_keypair() on the frontend works.
  2. Tried with CryptographyKey.from() instead of X25519PublicKey.from() – did not work.
  3. The getPublicKey() function returns an object wit buffer: Uint8Array(32) [ … ], while the public-key derived from sodium.crypto_box_keypair() returns an object with buffer: Uint8Array(32) [ … ], keyType: "x25519", publicKey: true.

Concept is based on:

  1. https://github.com/paragonie/sodium-plus/blob/master/docs/SodiumPlus/sealed-boxes.md
  2. https://dev.to/paragonie/message-encryption-in-javascript-and-php-cg9
  3. https://stackoverflow.com/a/34058638
paragonie-scott commented 4 years ago

https://github.com/paragonie/sodium-plus/commit/9d500c589b79cad850478ac427e6124267c6d73b

This was an oversight in the API design. CryptographyKey.from() was a catch-all, but didn't return a child object.

This will be fixed in v0.5.0.

paragonie-scott commented 4 years ago

Okay, v0.5.0 is out now. You'll want to use X25519PublicKey.from('...', 'hex') going forward.

weva-io commented 4 years ago

thanks a bunch Scott, no error in the console!

onurkose commented 4 years ago

I am using this approach in the opposite way;

This is sodium-plus.js side;

const selfKeyPair = await sodium.crypto_box_keypair(),
    selfPublicKey = await sodium.crypto_box_publickey(selfKeyPair),
    selfSecretKey = await sodium.crypto_box_secretkey(selfKeyPair),
    selfPublicKeyASCII = await sodium.sodium_bin2hex(selfPublicKey.getBuffer());

Then I pass selfPublicKeyASCII to PHP via http headers and use it inside a middleware to encrypt response body:

public function encryptContent($request, $content) {
    $publicKey = $request->header('Public-Key');
    $rawPublicKey = hex2bin($publicKey);
    $rawContent = sodium_crypto_box_seal($content, $rawPublicKey);
    return bin2hex($rawContent);
}

But when I try to decrypt on js side with this:

async function decryptTextBody (text) {
  let decrypted;

  try {
    decrypted = await sodium.crypto_box_seal_open(text, selfPublicKey, selfSecretKey)
  } catch (error) {
    console.warn(error)
  }

  return decrypted.toString('utf-8')
}

I receive Error: incorrect key pair for the given ciphertext error.

I also checked the encrypted php return value and js input data and they are exactly the same. I couldn't figure out where I am missing?

weva-io commented 4 years ago

Based on the provided code snippets, I am assuming that the text string you are passing to the decryptTextBody JS function is hexadecimal. You should convert it to binary before calling sodium.crypto_box_seal_open.

Other considerations/ suggestions (for your PHP code):

More info about utilities and helpers here.

onurkose commented 4 years ago

Thank you very much. Now it works perfectly.