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

Reply via email to