2

I have a code snippet in php which I would like to move into node.js but I cannot seem to find the right way to do it.

class  EncryptService
{
    const PUBLIC_CERT_PATH = 'cert/public.cer';
    const PRIVATE_CERT_PATH = 'cert/private.key';
    const ERROR_LOAD_X509_CERTIFICATE = 0x10000001;
    const ERROR_ENCRYPT_DATA = 0x10000002;

    public $outEncData = null;
    public $outEnvKey = null;
    public $srcData;

    public function encrypt()
    {
        $publicKey = openssl_pkey_get_public(self::PUBLIC_CERT_PATH);

        if ($publicKey === false) {
            $publicKey = openssl_pkey_get_public("file://".self::PUBLIC_CERT_PATH);
        }
        if ($publicKey === false) {
            $errorMessage = "Error while loading X509 public key certificate! Reason:";

            while (($errorString = openssl_error_string())) {
                $errorMessage .= $errorString . "\n";
            }
            throw new Exception($errorMessage, self::ERROR_LOAD_X509_CERTIFICATE);
        }

        $publicKeys = array($publicKey);
        $encData = null;
        $envKeys = null;
        $result = openssl_seal($this->srcData, $encData, $envKeys, $publicKeys);
        if ($result === false)
        {
            $this->outEncData = null;
            $this->outEnvKey = null;
            $errorMessage = "Error while encrypting data! Reason:";
            while (($errorString = openssl_error_string()))
            {
                $errorMessage .= $errorString . "\n";
            }
            throw new Exception($errorMessage, self::ERROR_ENCRYPT_DATA);
        }
        $this->outEncData = base64_encode($encData);
        $this->outEnvKey = base64_encode($envKeys[0]);
    }
};

The problem is that I cannot find an implementation of the openssl_sign in Javascript anywhere. I do need to keep this structure because I use both outEncData and outEnvKey.

I managed to find the equivalent implementation of openssl_sign with the crypto package but nothing for openssl_seal.

LE added working solution as an answer

8
  • By default openssl_seal seems to use RC4 cypher which is deprecated so you'll probably have to make various shenanigans to make it work. Commented Jan 29, 2020 at 16:46
  • But if the os has RC4 it looks like you should be able to replace this code with crypto.createCipheriv("RC4", myGeneratedKey, null) method nodejs.org/api/… Commented Jan 29, 2020 at 16:59
  • Thank you for the reply @Aivaras. I tried like you said and ended up with the following. const cypher = crypto.createCipheriv("RC4", public_key, null); let encrypted = cypher.update(message, 'utf8', 'base64'); encrypted += cypher.final('base64'); But it returns a hash that remains the same always and I am missing the env_key. In php the hash and env always changes for the same value. Is there something I did wrong in the code? Commented Jan 29, 2020 at 20:30
  • I think in php that env_key is generated behind the scenes and in Node you need to create it yourself and then use it instead of your public_key. Commented Jan 30, 2020 at 16:01
  • @Aivaras Can you please post a response with some code so I can look upon. Commented Jan 31, 2020 at 10:14

2 Answers 2

2

OK I've spent some time to figure this out, in short it is now in the repo: ivarprudnikov/node-crypto-rc4-encrypt-decrypt. But we want to follow SO rules here.

Below assumes that you have public key for signing the generated key and private key for testing if all is great.

  1. Randomly generated secret key used for encryption:
const crypto = require('crypto');

const generateRandomKeyAsync = async () => {
    return new Promise((resolve, reject) => {
        crypto.scrypt("password", "salt", 24, (err, derivedKey) => {
            if (err) reject(err);
            resolve(derivedKey.toString('hex'));
        });
    });
}
  1. Encrypt data with the generated key and then encrypt that key with a given public key. We want to send back both encrypted details and encrypted key as we expect the user on another side to have private key.
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');

const encryptKeyWithPubAsync = async (text) => {
    return new Promise((resolve) => {
        fs.readFile(path.resolve('./public_key.pem'), 'utf8', (err, publicKey) => {
            if (err) throw err;
            const buffer = Buffer.from(text, 'utf8');
            const encrypted = crypto.publicEncrypt(publicKey, buffer);
            resolve(encrypted.toString('base64'));  
        });
    });
}

const encryptStringAsync = async (clearText) => {
    const encryptionKey = await generateRandomKeyAsync();
    const cipher = await crypto.createCipheriv("RC4", encryptionKey, null);
    const encryptedKey = await encryptKeyWithPubAsync(encryptionKey);
    return new Promise((resolve, reject) => {
        let encryptedData = '';
        cipher.on('readable', () => {
          let chunk;
          while (null !== (chunk = cipher.read())) {
            encryptedData += chunk.toString('hex');
          }
        });
        cipher.on('end', () => {
          resolve([encryptedKey, encryptedData]); // return value
        });
        cipher.write(clearText);
        cipher.end();   
    });
}
  1. So now we can encrypt the details:
encryptStringAsync("foo bar baz")
   .then(details => {
        console.log(`encrypted val ${details[1]}, encrypted key ${details[0]}`);
    })

Will print something like:

encrypting foo bar baz
encrypted val b4c6c7a79712244fbe35d4, encrypted key bRnxH+/pMEKmYyvJuFeNWvK3u4g7X4cBaSMnhDgCI9iii186Eo9myfK4gOtHkjoDKbkhJ3YIErNBHpzBNc0rmZ9hy8Kur8uiHG6ai9K3ylr7sznDB/yvNLszKXsZxBYZL994wBo2fI7yfpi0B7y0QtHENiwE2t55MC71lCFmYtilth8oR4UjDNUOSrIu5QHJquYd7hF5TUtUnDtwpux6OnJ+go6sFQOTvX8YaezZ4Rmrjpj0Jzg+1xNGIIsWGnoZZhJPefc5uQU5tdtBtXEWdBa9LARpaXxlYGwutFk3KsBxM4Y5Rt2FkQ0Pca9ZZQPIVxLgwIy9EL9pDHtm5JtsVw==
  1. To test above assumptions it is necessary first to decrypt the key with the private one:
const decryptKeyWithPrivateAsync = async (encryptedKey) => {
    return new Promise((resolve) => {
        fs.readFile(path.resolve('./private_key.pem'), 'utf8', (err, privateKey) => {
            if (err) throw err;
            const buffer = Buffer.from(encryptedKey, 'base64')
            const decrypted = crypto.privateDecrypt({
                key: privateKey.toString(),
                passphrase: '',
            }, buffer);
            resolve(decrypted.toString('utf8'));
        });
    });
}
  1. After key is decrypted it is possible to decrypt the message:
const decryptWithEncryptedKey = async (encKey, encVal) => {
    const k = await decryptKeyWithPrivateAsync(encKey);
    const decipher = await crypto.createDecipheriv("RC4", k, null);
    return new Promise((resolve, reject) => {
        let decrypted = '';
        decipher.on('readable', () => {
          while (null !== (chunk = decipher.read())) {
            decrypted += chunk.toString('utf8');
          }
        });
        decipher.on('end', () => {
          resolve(decrypted); // return value
        });
        decipher.write(encVal, 'hex');
        decipher.end();
    });
}

Hope this answers the question.

Sign up to request clarification or add additional context in comments.

8 Comments

Hi and sorry for the late response I was away for the weekend. I tested it, theoretically it should work because all the steps are followed but it doesn't pass unfortunately . I'm trying to figure out what's wrong since this morning. I use this data to make a request to a 3rd party server. I switched your data encrypted to base64 and it still doesn't work. Every time the server returns that the data decryption failed.
Also I cannot test the decryption because it cannot read my private key, I get the following error Error: error:0407109F:rsa routines:RSA_padding_check_PKCS1_type_2:pkcs decoding error I added padding:crypto.constants.RSA_PKCS1_PADDING to the crypto.privateDecrypt method but it still doesn't work
I think the solution in this answer addressed the question. Given example successfully encrypts/decrypts on CI server. Further issues you're having can be addressed in another SO question along the lines "How to load private key ..." or "How to decrypt ..." or "Which encoding should I use in Buffer ...".
Ok, let me rephrase. It doesn't work. With the php the data and env_key can be decoded, as for the ones from js, they cannot be decoded, using exactly the same keys, so it doesn't really answer my question. It's an approach but not a valid solution
Unfortunately it does work as it runs publicly on CI server, not sure if you have checked. What I think happens in your case depends on certificates which is out of scope in this question. To test answered approach I've generated my own certificates as they were not provided in the question.
|
2

The final and working version that worked for me. My problem was that I used an 128bit random key encrypt the data, instead 256bit worked in the end.

The encryption works in JS and it can be decrypted in php with the openssl_open using your private key, which was what I asked in the original question.

const crypto = require('crypto');
const path = require('path');
const fs = require('fs');

const encryptMessage = (message) => {
  const public_key = fs.readFileSync(`${appDir}/certs/sandbox.public.cer`, 'utf8');
  const rc4Key = Buffer.from(crypto.randomBytes(32), 'binary');
  const cipher = crypto.createCipheriv('RC4', rc4Key, null);

  let data = cipher.update(message, 'utf8', 'base64');
  cipher.final();

  const encryptedKey = crypto.publicEncrypt({
    key: public_key,
    padding: constants.RSA_PKCS1_PADDING
  }, rc4Key);

  return {
    'data': data,
    'env_key': encryptedKey.toString('base64'),
  };
};

2 Comments

If you are looking for a more secure version with AES256, I've put up a gist here: gist.github.com/datashaman/d2f3b01196958b3adda9a62b5f75591c I suspect you need to add 'base64' to your final method call and append the results to the existing data.
it should be data += cipher.final() otherwise you'll end up losing some data :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.