Well.. I do charity work for a 5013C, which has limited resources and one of the requirements they asked of me was to be able to embed data in a static url.. Normally, this would just be a request parameter and my day would proceed on in a rather bland fashion.
This time, it was unique in that some of the data was sensitive (It applied a discount code to the price of a few items) - so manipulating that data would be impactful to the operational integrity of the application. So, how do you secure data when all you have is a url string (SSL and Post options were off the table as this was being embedded as a link in numerous other websites).
So, I decided to use a json payload and encrypt it using AES.. base64 encode it, urlEncode it... and dump that value along with the initial vector into the url as params. It was a rather small payload with only a few sensitive fields, so it was effective, yet crude..
I would be interested in knowing how others would handle this using only a url String making a RESTful call to a server.
The basic requirements:
1. It must be a RESTful url.. All the data is contained in the url.
2. Flat files and database stores are out of the question.. no way to persist data (This was the kicker for me)
3. It has to be embeddable and a direct call.. no middle man url's, etc..
I hope someone reads this and presents a graceful answer.. I don't feel that mine was the best answer, but given my tools in my toolbox.. I didn't see a better way.
Below is the code that does the lifting.. It's fairly straight forward:
Encrypting:
1. The object is serialized to a byte[]
2. Create a Cipher and a random initial vector
3. Hash the key using SHA-256 then trim it to 128-bit for the key
4. Base64 encode the encrypted value
5. Encode the Base64 value to a url friendly format
6. Return the encoded values to include as request parameters in the link
The link will look something like:
http://www.foo.com/view?a=PM90%2B0mzlDLpK5NKlG7CP5lgafMzn5qsrHulCRLR9ZkXnq9L5v7SMuEp%2BPWFtdNR1UxHGw%2Fxh7NzZSPtwSKNRCmQe8fNEMkJJuI4lJ3uvg0DrbI9xXALFZpUs6gAbO7SW5UCeHd6SYWMSWnM9agH9Mw13yfiCKNdiWPiW9j%2BhJa0J5lPraqF1x8srdfm48JaMEg1IaHtRLXukH3VjPTN8sGRUE8nzsu%2BGIyRLaHlXNKt5nZ4wAKtd9Dtfmwe4bgX4k7aEUpaYr90nCU67a%2BxX0NtDB6z7n5lkCQ7bQTNzSA%3D&b=Jux7I3tv%2Ff6HXXFrGOozZA%3D%3D
Decrypting:
1. We decode the request param from Base64 into the raw format. (The container takes care of the url encoding conversion.. so this is not a factor in TC Server)
2. Decode the intial vector value from Base64 into the raw format.
3. Configure the cipher using the secret and the initial vector that was passed in the url
4. Decrypt the encrypted data
5. Serialize the decrypted bytes back into the expected object
All this work.. due to a few limitations. Bleh.. But, it was a fun little traversal in cryptography in Java.
Below is a code sample setup to run in Spring MVC:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.test.web.controllers; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.ObjectInput; | |
import java.io.ObjectInputStream; | |
import java.io.ObjectOutput; | |
import java.io.ObjectOutputStream; | |
import java.net.URLEncoder; | |
import java.security.MessageDigest; | |
import java.security.SecureRandom; | |
import java.util.HashMap; | |
import java.util.Map; | |
import javax.crypto.Cipher; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
import javax.servlet.http.HttpServletResponse; | |
import org.apache.commons.codec.binary.Base64; | |
import org.springframework.stereotype.Controller; | |
import org.springframework.ui.Model; | |
import org.springframework.web.bind.annotation.PathVariable; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestMethod; | |
import org.springframework.web.bind.annotation.RequestParam; | |
@Controller | |
public class EncryptedViewController { | |
private String keyString = "adkj@#$02#@adflkj)(*jlj@#$#@LKjasdjlkj<.,mo@#$@#kljlkdsu343"; | |
protected final Logger logger = LoggerFactory.getLogger(this.getClass()); | |
@RequestMapping(value = "/view", method = RequestMethod.GET) | |
public String getDencryptedUrlView(@RequestParam String d, @RequestParam String v) throws Exception { | |
// The data goes out urlEncoded.. but comes in decoded (No Need to url decode on the request) | |
Object obj = decryptObject(d, v); | |
System.out.println("Object Decrypted: "+obj.toString()); | |
return "INDEX"; | |
} | |
@RequestMapping(value = "/{id}", method = RequestMethod.GET) | |
public String getEncryptedUrl(@PathVariable String id) { | |
Map<String,String> map = new HashMap<String,String>(); | |
map.put("id",id); | |
map.put("topSecret","Waffles are tasty"); | |
try { | |
String[] encrypted = encryptObject(map); | |
// url may differ.. based upon project initial context | |
System.out.println("http://localhost:8080/view?d="+encrypted[0]+"&v="+encrypted[1]); | |
}catch(Exception e) { | |
//logger.debug("Unable to encrypt view id: "+id, e); | |
} | |
return "INDEX"; | |
} | |
/** | |
* Encrypts and encodes the Object and IV for url inclusion | |
* @param input | |
* @return | |
* @throws Exception | |
*/ | |
private String[] encryptObject(Object obj) throws Exception { | |
ByteArrayOutputStream stream = new ByteArrayOutputStream(); | |
ObjectOutput out = new ObjectOutputStream(stream); | |
try { | |
// Serialize the object | |
out.writeObject(obj); | |
byte[] serialized = stream.toByteArray(); | |
// Setup the cipher and Init Vector | |
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | |
byte[] iv = new byte[cipher.getBlockSize()]; | |
new SecureRandom().nextBytes(iv); | |
IvParameterSpec ivSpec = new IvParameterSpec(iv); | |
// Hash the key with SHA-256 and trim the output to 128-bit for the key | |
MessageDigest digest = MessageDigest.getInstance("SHA-256"); | |
digest.update(keyString.getBytes()); | |
byte[] key = new byte[16]; | |
System.arraycopy(digest.digest(), 0, key, 0, key.length); | |
SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); | |
// encrypt | |
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); | |
// Encrypt & Encode the input | |
byte[] encrypted = cipher.doFinal(serialized); | |
byte[] base64Encoded = Base64.encodeBase64(encrypted); | |
String base64String = new String(base64Encoded); | |
String urlEncodedData = URLEncoder.encode(base64String,"UTF-8"); | |
// Encode the Init Vector | |
byte[] base64IV = Base64.encodeBase64(iv); | |
String base64IVString = new String(base64IV); | |
String urlEncodedIV = URLEncoder.encode(base64IVString, "UTF-8"); | |
return new String[] {urlEncodedData, urlEncodedIV}; | |
}finally { | |
stream.close(); | |
out.close(); | |
} | |
} | |
/** | |
* Decrypts the String and serializes the object | |
* @param base64Data | |
* @param base64IV | |
* @return | |
* @throws Exception | |
*/ | |
public Object decryptObject(String base64Data, String base64IV) throws Exception { | |
// Decode the data | |
byte[] encryptedData = Base64.decodeBase64(base64Data.getBytes()); | |
// Decode the Init Vector | |
byte[] rawIV = Base64.decodeBase64(base64IV.getBytes()); | |
// Configure the Cipher | |
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | |
IvParameterSpec ivSpec = new IvParameterSpec(rawIV); | |
MessageDigest digest = MessageDigest.getInstance("SHA-256"); | |
digest.update(keyString.getBytes()); | |
byte[] key = new byte[16]; | |
System.arraycopy(digest.digest(), 0, key, 0, key.length); | |
SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); | |
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); | |
// Decrypt the data.. | |
byte[] decrypted = cipher.doFinal(encryptedData); | |
// Deserialize the object | |
ByteArrayInputStream stream = new ByteArrayInputStream(decrypted); | |
ObjectInput in = new ObjectInputStream(stream); | |
Object obj = null; | |
try { | |
obj = in.readObject(); | |
}finally { | |
stream.close(); | |
in.close(); | |
} | |
return obj; | |
} | |
} |