Better Living Through Transfer and ColdSpring

We have a live system with customer data and a new requirement comes along that a particular piece of customer data must be encrypted in the database from now on. We already encrypt some columns (using Triple DES - which you might have guessed given the recent posts on my blog and here about mimicking ColdFusion's encryption in Java/Groovy). What is the smallest possible code change to ensure that as any user updates their data in future, this item will automatically be encrypted - whilst still handling the case of legacy data being unencrypted?

We use Transfer for all our persistent business objects and almost all of our business objects have a decorator defined (for validation or some additional business logic). We also use Brian's TDOBeanInjectorObserver to automatically inject services into our business objects - just add a setter for a service and the bean injector takes care of the rest.

Here's the bean injector definition in our ColdSpring file:


<bean id="transferObjectInjector" class="coldspring.transfer.TDOBeanInjectorObserver">
    <constructor-arg name="transfer"><ref bean="transfer" /></constructor-arg>
    <constructor-arg name="suffixList"><value>service,datasource</value></constructor-arg>
    <constructor-arg name="debugMode"><value>true</value></constructor-arg>
</bean>
Normally you would declare it non-lazy but we already do other non-lazy initialization so in our ColdSpring factory initialization code, we do this:

<cfset bf.getBean("transferObjectInjector") />
to force the bean injector to be initialized which, in turn, registers itself as a Transfer event listener (so that it can intercept object creation). The suffixList specifies that any set*Service() method or set*Datasource() method on the business objects managed by Transfer should be matched to beans defined in ColdSpring and injected.

So how do we add the on-demand encryption to our business object's data?

First, we add a setter to the Transfer object decorator (our business object) to match our encryption service:


function setEncryptionService(encryptionService) {
    variables.encryptionService = encryptionService;
}
(Yes, I use cfscript a lot these days)

Then we override the setter for the data we need to encrypt:


function setSFPassword(password) {
    getTransferObject().setSFPassword( variables.encryptionService.encryptStr(password) );
}
This uses the (injected) encryption service to encrypt the (user-submitted) password and then pass it to the underlying setter (generated by Transfer). That was easy. But what about the getter? That has to handle unencrypted data as well as encrypted data.

Here's the matching setter:


function getSFPassword() {
    var password = getTransferObject().getSFPassword();
    var decrypted = "";
            
    try {
        decrypted = variables.encryptionService.decryptStr(password);
        if (decrypted == "") decrypted = password;
        return decrypted;
    } catch (any ex) {
        // must be unencrypted already
        return password;
    }
}
We use the underlying getter to retrieve the value from the database and then we try to decrypt it. If decryption fails, we assume the data is currently unencrypted and return it as-is. Note that decryption can fail in two ways: it can throw an exception or it can return an empty string!

This allows us to encrypt data upon any user updates while still handled unencrypted legacy data during the migration period.

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Michael Sharman's Gravatar Hi Sean,

>We also use Brian's TDOBeanInjectorObserver to automatically inject services into our business objects - just

Could you explain this a bit further please? I would have thought your application controller would make a call to the relevant service layer passing in any business object it needed. Either that or the service layer will get the business object from a factory (or simply instantiate it itself etc).

From there the Service can perform any application/business logic and possibly call a method on the business object which may or may not have some sort of DAO/Gateway composed with in to handle database operations. The other common approach being to pass the business object (from the Service layer) into a DAO/Gateway etc

I've never considered composing a Service layer into and actual business object. So any further info would be much appreciated :)
# Posted By Michael Sharman | 8/17/08 2:01 AM
Elliott Sprehn's Gravatar @Sean

Wouldn't it be easier to just try/catch in the EncryptionService and return "", or check for "" in there and throw an exception so you didn't need the double check everywhere?

@Michael

That's actually a fairly common technique for factoring out behavior for business objects (at least in the OO CF world, and certainly in Transfer users, probably in Java).

The goal being to attach as rich as possible an API onto the BOs, but still break the logic up into services that can be reused/remoted/accessed/etc. directly if needed.

It honestly feels more natural to me too, since we want to ask objects to do stuff for us in the application and make that the entry point; thus we should talk to the objects, not to some services that hide them. If all we do is talk to the services and not the objects, then why not just use structs for the BOs?

From the super BO/ORM purist perspective putting the DAO or the Gateway into the object is a serious error too. Even having getTransfer() inside the object is considered a mistake. From this perspective objects are merely representations of some kind of data that talk to other objects (in classical sense, like an ArrayList, not like a Model) and Services and have no knowledge of the fact that they're special (ie in a Database and would need a Gateway, or would even require special treatment for persistence). This approach is highly unpractical (in CF and others) for a lot of tasks and reasons, so we cheat now and again, like with getTransfer().

Of course there's a number of other approaches like ActiveRecord that take a different look at how this should all work. :)
# Posted By Elliott Sprehn | 8/17/08 7:11 AM
Sean Corfield's Gravatar @Elliott, yes, in the next refactoring we'll evaluate all uses of decryptStr() and see what would be the most appropriate way to handle the empty string failure. The encryption service is used in a lot of places and right now there aren't enough unit tests for me to be sure of the effects of changing how the decryptStr() method behaves :(

@Michael, Elliott's answer covers much of what I would have said and I've asked Brian to comment as well since he wrote the bean injector.

My stance is to inject services that a business object needs in order for it to transparently "do its job" and be as rich a business object as possible. My services generally serve only two purposes: to provide broad (system-wide) operations (like encryption) that can be used by many business objects; or to provide an API onto the business model in general that can be exposed to clients (i.e., a service-as-remote-facade or a service exposed to a framework-specific controller, such as in Model-Glue).
# Posted By Sean Corfield | 8/17/08 1:08 PM
Brian Kotek's Gravatar @Michael: Elliott also covers most of what I would have to say. Composing services into business objects gives the business object a much richer set of behavior, but still allows you to keep the objects cohesive and allow many kinds of business objects to leverage a common set of generalized service methods.

Without this kind of rich business object, you quickly get into the "Anemic Domain Model" antipattern, where your BOs are nothing but dumb data containers and all of the business logic resides in the service layer. Since many CF'ers tend to come from procedural coding backgrounds, breaking out of this approach can be a challenge. But I've found that at the end of the day, making the BOs the central player in your model results in a much more flexible system that is much easier to change over time.
# Posted By Brian Kotek | 8/17/08 5:47 PM
Damien's Gravatar How are you doing big searches with all this encrypted data using transfer. TQL, readByPropertyMap, etc?
# Posted By Damien | 8/24/08 3:08 PM
Sean Corfield's Gravatar @Damien, we don't search against passwords etc.

At login, we encrypt the username / password submitted and use readByPropertyMap() to retrieve the single matching author record. In our admin console, we can search by email address but use a full exact match (so again, we can use the encrypted value as a key).
# Posted By Sean Corfield | 8/24/08 5:46 PM
Damien's Gravatar I understand the password stuff. I was curious if you guys encrypt other data going in and out of the database? Using transfer of course. Maybe using a decorator with transfer to encrypt all the info as it gets setted/getted. Not to hard to generate that using the illudium generator. But maybe you don't encrypt standard data. But if you did use transfer to search encrypted data, thats what I'd be curious to see how.
# Posted By Damien | 8/24/08 6:00 PM
Sean Corfield's Gravatar @Damien, we encrypt what we need to for security but we don't encrypt things we might want to do partial match searches on (although mostly we use the Google Search Appliance to crawl content so it sees unencrypted data anyway).
# Posted By Sean Corfield | 8/24/08 6:15 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner