Using OpenSSL, RSA and RC4 to exchange encrypted data from PHP to Java

 

Needed a mechanism to be able to pass chunks of data securely from PHP to Java with requirements like;

  • If it accidentally ends up in a log file, it should be unusable
  • The system doing the encryption (web server, PHP) should be able to do so without exposing the means to decrypt the data i.e. we’re talking about some form of public-key cryptography.
  • On the PHP side, it needs to be fast and simple.
  • It shouldn’t result in significant extra work in maintaining public / private key pairs.

One solution might be something “from scratch” involving mcrypt or PHP libraries like Crypt_RSA. Do-able but more glue code you have, the greater the chance of unwittingly leaving security holes.

Another approach is GnuPG, either via the command line as discussed in this tutorial or via the GnuPG extension from PECL. While GnuPG is a solid way to do it, forking sub processes is ugly on public web servers and installing new extensions makes work. What’s more, we’re then have to maintain another set of public/private key pairs and we’re already using SSL…

Option 3 is using OpenSSL and PHP’s openssl_seal() function. SSL is normally used for encrypting networked communication between peers but that’s not all it can do. The OpenSSL extension has been a default part of the PHP distribution for some time now (since PHP 5.0.0 if I remember right), so no need to make work for sysadmins. It’s also a single function for the caller – fast and easy. What’s more – and perhaps the biggest win – it also allows us to re-use existing SSL certificates.

OpenSSL EVP

It’s worth having a grasp of what PHP’s openssl_seal() actually does – it’s really just a wrapper on OpenSSL’s EVP encryption envelope API. The way EVP works is something like this;

  1. It generates a random number (call it evk)
  2. It encrypts evk using a public key (e.g. from your web servers public SSL certificate) – call this Xevk. The key is encrypted using RSA. Note that though OpenSSL supports DSA certs, openssl_seal() doesn’t – tried it, got an error reporting RSA as required.
  3. Now it encrypt the data itself, using a symmetric key algorithm – the original evk is used as the key. Note the symmetric encryption alogithm is currently fixed as RC4 although the latest source suggests that may become something you can select in future PHP versions, using a fifth argument to openssl_seal().

The reason for using RSA and RC4 here is essentially because asymmetric encryption algorithms are generally expensive in terms of performance – you don’t want to encrypt big chunks of data this way or you’ll be in for a loooong wait. Instead RSA is used to encrypt only the key to the (much) faster symmetric algorithm. We then pass the encrypted data and the encrypted key to their recipient, where we have access to the private key available and can take the following steps;

  1. Load the SSL private key
  2. Decrypt Xevk using RSA and the private key to recover evk
  3. Decrypt the data using RC4 and key evk.

The PHP

The following example shows how it works in PHP using the OpenSSL extension API;


<?php
$dir = '/path/to/openssl/certificates';
$data = "A secret message for Bob"; // the data to encrypt

print "<pre>";
print "Input Data:\t$data\n";

// Get the public key
$publicKey = openssl_get_publickey(file_get_contents("$dir/pubkey_rsa.pem"));

// encrypt the data
openssl_seal($data, $sealed, $ekeys, array($publicKey));

openssl_free_key($publicKey);

$sealed = base64_encode($sealed);
$Xevk = base64_encode($ekeys[0]);

print "Encypted data:\t$sealed\n";
print "Xevk:\t\t$Xevk\n";

// Now do the round trip for example - decrypt using the private key
$privateKey = openssl_get_privatekey(file_get_contents("$dir/privkey_rsa.pem"));

openssl_open(base64_decode($sealed), $opened, base64_decode($Xevk), $privateKey)
or die(openssl_error_string());
openssl_free_key($privateKey);

print "Decypted data:\t$opened\n";
?>

Which, when executed, should give you output looking something like this;

Input Data:	A secret message for Bob
Encypted data:	E9u4eKgagNpScHUa9BzITQDJu72oGAf5
Xevk:		z81ZHFHc7pcmXRKM/7HulfFQvjPS9wZr63q1MgBMCcvYdVKmIA/FercSyeH6EZu8+APDDpq3kyUMLB1gW6UaRg==
Decypted data:	A secret message for Bob

Note that “Encypted data” and “Xevk” here are Base64 encoded and I can pass them around easily in this form.

The Java

To decrypt, the first working implementation looks like;


package ch.local.common.util.crypto;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.util.encoders.Base64;

/**
* For decrypting data encrypted with PHP's openssl_seal()
*
* Example - String envKey is the Base64 encoded, RSA encrypted envelop key
* and String sealedData is the Base64 encoded, RC4 encrypted payload, which
* you got from PHP's openssl_seal() then;
*
* <pre>
* KeyPair keyPair = OpenSSLInterop.keyPairPEMFile(
* "/path/to/openssl/privkey_rsa.pem"
* );
*
* PrivateKey privateKey = keyPair.getPrivate();
* String plainText = OpenSSLInterop.decrypt(sealedData, envKey, privateKey);
* </pre>
*
* @see http://www.php.net/openssl_seal
* @author Harry Fuecks
* @since Oct 25, 2007
* @version $Id$
*/
public class OpenSSLInterop {

private static boolean bcInitialized = false;

/**
* @param cipherKey
* @param cipherText
* @param privateKey
* @return
* @throws Exception
*/
public static String decrypt(String cipherText, String cipherKey, PrivateKey privateKey)
throws Exception {

return decrypt(cipherText, cipherKey, privateKey, "UTF-8");

}

/**
* @param cipherKey Base64 encoded, RSA encrypted key for RC4 decryption
* @param cipherText Base64 encoded, RC4 encrypted payload
* @param privateKey
* @param charsetName
* @return decrypted payload
* @throws Exception
*/
public static String decrypt(String cipherText, String cipherKey, PrivateKey privateKey, String charsetName)
throws Exception {

byte[] plainKey = decryptRSA(Base64.decode(cipherKey), privateKey);
byte[] plaintext = decryptRC4(plainKey, Base64.decode(cipherText));
return new String(plaintext, charsetName);

}

/**
* Loads a KeyPair object from an OpenSSL private key file
* (Just wrapper around Bouncycastles PEMReader)
* @param filename
* @return
* @throws Exception
*/
public static KeyPair keyPairFromPEMFile(String filename)
throws Exception {

if ( !bcInitialized ) {
Security.addProvider(new BouncyCastleProvider());
bcInitialized = true;
}

FileReader keyFile = new FileReader(filename);
PEMReader pemReader = new PEMReader(keyFile);
return (KeyPair)pemReader.readObject();

}

/**
* Returns a KeyPair from a Java keystore file.
*
* Note that you can convert OpenSSL keys into Java Keystore using the
* "Not yet commons-ssl" KeyStoreBuilder
* See - http://juliusdavies.ca/commons-ssl/utilities/
* e.g.
*
* $ java -cp not-yet-commons-ssl-0.3.8.jar org.apache.commons.ssl.KeyStoreBuilder \
* "privkey password" ./privkey.pem ./pubkey.pem
*
* @param filename
* @param alias
* @param keystorePassword
* @return
*/
public static KeyPair keyPairFromKeyStore(String filename, String alias, String keystorePassword)
throws Exception {

KeyStore keystore = KeyStore.getInstance("JKS");
File keystoreFile = new File(filename);

FileInputStream in = new FileInputStream(keystoreFile);
keystore.load(in, keystorePassword.toCharArray());
in.close();

Key key = keystore.getKey(alias, keystorePassword.toCharArray());
Certificate cert = keystore.getCertificate(alias);
PublicKey publicKey = cert.getPublicKey();

return new KeyPair(publicKey, (PrivateKey)key);

}

/**
* @param cipherKey
* @param privateKey
* @return
* @throws Exception
*/
private static byte[] decryptRSA(byte[] cipherKey, PrivateKey privateKey) throws Exception {

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(cipherKey);

}

/**
* Defaults to UTF-8 as the output encoding
* @param plainKey Base64 encoded, RSA encrypted key for RC4 decryption
* @param cipherText Base64 encoded, RC4 encrypted payload
* @return decrypted payload
* @throws Exception
*/
private static byte[] decryptRC4(byte[] plainKey, byte[] cipherText)
throws Exception {

SecretKey skeySpec = new SecretKeySpec(plainKey, "RC4");
Cipher cipher = Cipher.getInstance("RC4");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
return cipher.doFinal(cipherText);

}

}

Usage is simple…

KeyPair keyPair = OpenSSLInterop.keyPairPEMFile("/path/to/openssl/privkey_rsa.pem");
PrivateKey privateKey = keyPair.getPrivate();
String plainText = OpenSSLInterop.decrypt(sealedData, envKey, privateKey);

You could also use a keystore, for which the keyPairFromKeyStore() provides a short cut. Note that a fairly friendly way to convert PEM certificates to keystores is with Not Yet commons-ssl, using the KeyStoreBuilder e.g.

$ java -cp not-yet-commons-ssl-0.3.8.jar org.apache.commons.ssl.KeyStoreBuilder\
 "privkey password" /path/to/privkey.pem /path/to/pubkey.pem

In fact Not Yet commons-ssl also has a mechanism to decrypt ciphertexts generated by openssl’s password based encryption (PBE), used on the command line. It won’t work with output from PHP’s openssl_seal() though, because the PBE form makes use of a random salt, which it also places in the encrypted output.

The dependency on BouncyCastle (you need a bcprov[xxx].jar) is really only needed for reading .pem files – it can be eliminated fairly easily if you’re planning only on using Java keystores – you’d just need to use a different Base64 implementation.

By the way, no encyrpt method at this time because we don’t need it right now, but shouldn’t be too hard to implement.

 
 

9 Kommentare

  1. PHPDeveloper.org
    30. Oct 2007, 13:33
     
  2. php devblog
    13. Nov 2007, 07:45

    PHP is so easy :)

     
  3. yugan
    15. Dec 2008, 17:26

    Harry Fuecks, thanks for the wonderful article. I actually need the encryption part. do you have an article on how to encrypt using java? The two things i am not able to find are:
    1. how to create the PublicKey object from a public key string I have.
    2. How can I do a base64 encode using java.

    any help is highly appreciated

     
  4. Harry Fuecks
    16. Dec 2008, 14:11

    Hi Yugan,

    I haven’t needed to go in the Java to PHP direction (so good luck). It’s been a while since I’ve had to deal with this stuff but off the top of my head;

    1. You can load a public key from a file the same way as a private key i.e. as done in the OpenSSLInterop.keyPairFromKeyStore() method in the example above – when you call KeyStore.load() the password param. is apparently optional (try null or an empty string) – KeyStore is designed for both public and private keys.

    2. There’s a bunch of implementations around (why it can’t be available by default I don’t know) e.g. http://iharder.sourceforg…current/java/base64/

    Hope that helps a little.

     
  5. endyk
    17. Feb 2010, 08:54

    hi, thanks for your nice articles
    btw, how the code if we build sealed encrypt from java then decrypt with php openssl
    thankyou

     
  6. dim
    7. Feb 2011, 06:40

    can u pls tell me how to do openssl with rsa at the client side using javascript. ? i hav used php with openssl at the server side

     
  7. teds woodworking discount
    11. Jun 2011, 23:50

    yeah! PHP is so easy :)

     
  8. tinnitus control
    13. Jun 2011, 10:57

    well i use often openssl and php but never java :)

     
  9. Custom Banner Design
    7. Sep 2011, 09:26

    Very informative post. Keep up the good work. I would really look forward to your other posts

     

Sorry, comments for this entry are closed at this time.