/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ws.extensions.security;

import java.util.Collection;
import java.util.HashSet;

import javax.crypto.SecretKey;

import org.apache.xml.security.encryption.XMLCipher;
import org.apache.xml.security.encryption.XMLEncryptionException;
import org.jboss.ws.extensions.security.element.EncryptedKey;
import org.jboss.ws.extensions.security.element.ReferenceList;
import org.jboss.ws.extensions.security.element.SecurityHeader;
import org.jboss.ws.extensions.security.element.SecurityProcess;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class DecryptionOperation implements DecodingOperation
{

   private SecurityHeader header;

   private SecurityStore store;

   public DecryptionOperation(SecurityHeader header, SecurityStore store) throws WSSecurityException
   {
      this.header = header;
      this.store = store;
   }

   private boolean isContent(Element element)
   {
      return Constants.XENC_CONTENT_TYPE.equals(element.getAttribute("Type"));
   }

   private String getEncryptionAlgorithm(Element element) throws WSSecurityException
   {
      element = Util.findElement(element, "EncryptionMethod", Constants.XML_ENCRYPTION_NS);
      if (element == null)
         throw new InvalidSecurityHeaderException("Encrypted element corrupted, no encryption method");

      String alg = element.getAttribute("Algorithm");
      if (alg == null || alg.length() == 0)
         throw new InvalidSecurityHeaderException("Encrypted element corrupted, no algorithm specified");

      return alg;
   }

   private String decryptElement(Element element, SecretKey key) throws WSSecurityException
   {
      Element previous;
      boolean parent;
      boolean isContent;

      // We find the decrypted element by traversing to the element before the
      // encrypted data. If there is no sibling before the encrypted data, then
      // we traverse to the parent.
      // "Now take a step back . . . and then a step forward . . . and then a
      // step back . . . and then we're cha-chaing." -Chris Knight
      parent = isContent = isContent(element);
      if (parent)
      {
         previous = (Element) element.getParentNode();
      }
      else
      {
         previous = Util.getPreviousSiblingElement(element);
         if (previous == null)
         {
            parent = true;
            previous = (Element) element.getParentNode();
         }
      }

      String alg = getEncryptionAlgorithm(element);
      try
      {
         XMLCipher cipher = XMLCipher.getInstance(alg);
         cipher.init(XMLCipher.DECRYPT_MODE, key);
         cipher.doFinal(element.getOwnerDocument(), element);
      }
      catch (XMLEncryptionException e)
      {
         throw new FailedCheckException("Decryption was invalid.");
      }
      catch (Exception e)
      {
         throw new WSSecurityException("Could not decrypt element: " + e.getMessage(), e);
      }

      if (isContent)
         return Util.getWsuId(previous);

      Element decrypted = (parent) ? Util.getFirstChildElement(previous) : Util.getNextSiblingElement(previous);
      if (decrypted == null)
         return null;

      return Util.getWsuId(decrypted);
   }

   private boolean isEncryptedData(Element element)
   {
      return "EncryptedData".equals(element.getLocalName()) && Constants.XML_ENCRYPTION_NS.equals(element.getNamespaceURI());
   }

   public Collection<String> process(Document message, SecurityProcess process) throws WSSecurityException
   {
      Collection<String> ids = new HashSet<String>();
      EncryptedKey key = (EncryptedKey) process;
      ReferenceList list = key.getReferenceList();
      for (String uri : list.getAllReferences())
      {
         Element element = Util.findElementByWsuId(message.getDocumentElement(), uri);
         if (element == null)
            throw new WSSecurityException("A reference list refered to an element that was not found: " + uri);

         if (!isEncryptedData(element))
            throw new WSSecurityException("Malformed reference list, a non encrypted data element was referenced: " + uri);

         ids.add(decryptElement(element, key.getSecretKey()));
      }

      return ids;
   }
}
