A couple of weeks back, I posted for (and received) assistance in
signing a document with the UsernameToken. I've recently worked out a
method to do a related operation: encrypting (and decrypting) a message
with that same security reference. Unfortunately, it wasn't as
straightforward.
I'm posting my results to the list partly as a record for anyone trying
to do this in the future and partly in the hopes that someone may
eventually try to roll this stuff into the main code base. I'm not
submitting a patch myself because I'm pretty sure I've broken
non-UsernameToken based encryption and because of my unfamiliarity with
the code base.
This will be quite a long message, so unless you've got an unhealthy
fascination with this sort of encryption it may be a good time to go to
the next message.
Part 1: Encryption
Here's the client-side code I used to encrypt my message. Things like
the document and usernameToken have already been defined - see my
earlier postings.
Reference reference = new Reference(document);
reference.setURI("#" + ut.getId());
reference.setValueType("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken");
SecurityTokenReference securityTokenReference = new
SecurityTokenReference(document);
securityTokenReference.setReference(reference);
WSSecurityUtil.setNamespace(securityTokenReference.getElement(),
WSConstants.WSSE_NS, WSConstants.WSSE_PREFIX);
WSSecEncrypt secEncrypt = new WSSecEncrypt();
secEncrypt.setKeyIdentifierType(WSConstants.EMBED_SECURITY_TOKEN_REF);
secEncrypt.setSecurityTokenReference(securityTokenReference);
secEncrypt.setKey(secretKey);
secEncrypt.setSymmetricEncAlgorithm(WSConstants.AES_128);
secEncrypt.setDocument(document);
// this specifies that the encryptMe element should be encrypted, but
// it could be any combination of elements that gets encrypted
Vector<WSEncryptionPart> encryptionParts = new Vector<WSEncryptionPart>();
String name = "encryptMe";
String namespace = null;
String encMod = "Content";
WSEncryptionPart encryptionPart = new WSEncryptionPart(name, namespace, encMod);
encryptionParts.add(encryptionPart);
secEncrypt.setParts(encryptionParts);
secEncrypt.build(document, null, secHeader);
This generates a SOAP message that has been encrypted. It looks something like
this (I removed the digital signature fields to make things a little more
readable):
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#" >
<SOAP-ENV:Header>
<wsse:Security SOAP-ENV:mustUnderstand="1">
<wsse:UsernameToken wsu:Id="UsernameToken-14721926">
<wsse:Username>pierre</wsse:Username>
<wsse:Password
Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">7qSMV2ole4ftWtvaxFMBYX5/YLc=</wsse:Password>
<wsse:Nonce>jaXk03JWGVBsxJ1TKv0z7Q==</wsse:Nonce>
<wsu:Created>2008-08-27T21:42:43.018Z</wsu:Created>
</wsse:UsernameToken>
<xenc:ReferenceList>
<xenc:DataReference URI="#EncDataId-1281123"/>
</xenc:ReferenceList>
</wsse:Security>
</SOAP-ENV:Header>
<SOAP-ENV:Body
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
wsu:Id="id-20000831">
<helloWorld>
<stuff>The following bit is encrypted</stuff>
<encryptMe>
<xenc:EncryptedData Id="EncDataId-1281123"
Type="http://www.w3.org/2001/04/xmlenc#Content"
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:EncryptionMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:Reference URI="#UsernameToken-14721926"
ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:CipherValue
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">5lEPD1JcaArVNk9imTSsHLW7n8lTY8F1HGmdkU2gZSNtosRtdA9E3umEjXp4xyeJ</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</encryptMe>
</helloWorld>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
As hoped, only the contents of the <encryptMe> element were encrypted.
Part 2: Decryption
When I attempted to decrypt this message with the standard CXF action,
I ran into two problems. The first is that the "Encrypt" action always
forces a lookup of the crypto properties which I didn't have specified
since it wasn't needed. The second was that WSS4J's ReferenceListProcessor
was not set up to handle security references of UsernameToken.
I solved both these problems by creating custom versions of the
WSS4JInInterceptor and ReferenceListProcessor. Subclassing would have
been preferrable, but the classes were a little resistant to easy
subclassing due to method size and visibility issues.
Here are my changes to WSS4JInInterceptor:
public WSS4JInInterceptor() {
super();
setPhase(Phase.PRE_PROTOCOL);
getAfter().add(SAAJInInterceptor.class.getName());
// use our custom ReferenceListProcessor
// this is a little sloppy, see note below
secEngine.getWssConfig().setProcessor(
WSSecurityEngine.REFERENCE_LIST,
ca.intelliware.common.webservice.ReferenceListProcessor.class.getName());
}
[...]
public void handleMessage(SoapMessage msg) throws Fault {
[...]
// SS: we remove "Encrypt" from the actions before we pass it into
decodeAction.
// The trick here is that if doAction includes the encrypt bit
value, lower-level code
// automatically tries to look up our crypto information and throws
an exception. So
// we use two sets of values here: doAction has the Encrypt action
removed, while actions
// still has it since that is the one that is compared to the
wsResult vector later on.
String noEncryptAction = StringUtils.remove(action, "Encrypt");
int doAction = WSSecurityUtil.decodeAction(noEncryptAction, new
Vector());
WSSecurityUtil.decodeAction(action, actions);
[...]
Note that the above code is actually pretty sloppy. secEngine
is basically a static field that I'm modifying for my purposes.
However, since I know I'm the only one using it I can do so without
feeling too bad about it.
There's is an alternate approach to changing up the processors that
get mapped to different actions: WSS4JInInterceptor has a second constructor
that takes in a Map of processors to override the defaults. But since I had
to modify WSS4JInInterceptor anyway to avoid the crypto lookup, I thought
I'd just put the other reference in the constructor.
And here are the changes I made to ReferenceListProcessor:
private SecretKey getKeyFromSecurityTokenReference(Element secRefToken, String
algorithm,
Crypto crypto, CallbackHandler cb) throws WSSecurityException {
[...]
if (p == null
|| (!(p instanceof EncryptedKeyProcessor)
&& !(p instanceof DerivedKeyTokenProcessor)
&& !(p instanceof SAMLTokenProcessor)
// SS support this type of processor now too
&& !(p instanceof UsernameTokenProcessor))) {
[...]
if(p instanceof EncryptedKeyProcessor) {
[...]
} else if(p instanceof DerivedKeyTokenProcessor) {
[...]
} else if(p instanceof SAMLTokenProcessor) {
[...]
} else if(p instanceof UsernameTokenProcessor) {
// SS added UsernameToken handling
UsernameTokenProcessor utp = (UsernameTokenProcessor) p;
decryptedData = utp.getUt().getSecretKey();
}
[...]
With these changes, I was able to encrypt and decrypt messages using the
UsernameToken security reference. However, in doing so I removed the ability
of the service to properly handle any other sort of encryption. It's more of
a hack than anything else, but it works for my implementation. Hopefully
someone will find it of use in the future.
I'm also open to the possibility that there's an easier way to do this, and
would appreciate any feedback if that is the case.
-Steve