/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jackrabbit.vault.fs.config;

import java.io.ByteArrayOutputStream;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.jcr.Credentials;
import javax.jcr.SimpleCredentials;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.jackrabbit.vault.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

/**
 * {@code SimpleCredentialsConfig}...
*
*/
public class SimpleCredentialsConfig extends CredentialsConfig {

    /**
     * key length
     */
    private final static int KEY_LENGTH = 8;

    /**
     * encryption prefix
     */
    private final static String PREFIX = "{DES}";

    /**
     * default logger
     */
    private static final Logger log = LoggerFactory.getLogger(SimpleCredentialsConfig.class);

    private final SimpleCredentials creds;
    public static final String ELEM_USER = "user";
    public static final String ATTR_NAME = "name";
    public static final String ATTR_PASSWORD = "password";

    public SimpleCredentialsConfig(SimpleCredentials creds) {
        super("simple");
        this.creds = creds;
    }

    public Credentials getCredentials() {
        return creds;
    }

    public static SimpleCredentialsConfig load(Element elem) throws ConfigurationException {
        assert elem.getNodeName().equals(ELEM_CREDETIALS);

        NodeList nl = elem.getChildNodes();
        for (int i=0; i<nl.getLength(); i++) {
            Node child = nl.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                if (child.getNodeName().equals(ELEM_USER)) {
                    Element e = (Element) child;
                    String name = e.getAttribute(ATTR_NAME);
                    String pass = decrypt(e.getAttribute(ATTR_PASSWORD));
                    return new SimpleCredentialsConfig(
                            new SimpleCredentials(
                                    name,
                                    pass == null ? new char[0] : pass.toCharArray()));
                }
            }
        }
        throw new ConfigurationException("mandatory element <user> missing.");
    }

    @Deprecated
    public void writeInner(ContentHandler handler) throws SAXException {
        throw new UnsupportedOperationException("No longer supports write with a SAX contentHandler, user write with XMLStreamWriter instead!");
    }

    
    @Override
    protected void writeInner(XMLStreamWriter writer) throws XMLStreamException {
        if (creds != null) {
            writer.writeStartElement(ELEM_USER);
            writer.writeAttribute(ATTR_NAME, creds.getUserID());
            writer.writeAttribute(ATTR_PASSWORD, encrypt(new String(creds.getPassword())));
            writer.writeEndElement();
        }
    }

    /**
     * Encrypts the given string in a fairly secure way so that it can be
     * {@link #decrypt(String) decrypted} again.
     *
     * @param s string to encrypt
     * @return the encrypted string with a "{AES}" prefix.
     */
    private static String encrypt(String s) {
        try {
            SecretKey key = KeyGenerator.getInstance("DES").generateKey();
            Cipher cipher = Cipher.getInstance("DES");
            byte[] keyBytes = key.getEncoded();
            byte[] data = s.getBytes("utf-8");
            ByteArrayOutputStream out = new ByteArrayOutputStream(keyBytes.length + data.length);
            out.write(keyBytes);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            out.write(cipher.update(data));
            out.write(cipher.doFinal());
            StringBuilder ret = new StringBuilder(PREFIX);
            for (byte b: out.toByteArray()) {
                ret.append(Text.hexTable[b>>4 & 0x0f]).append(Text.hexTable[b&0x0f]);
            }
            return ret.toString();
        } catch (Exception e) {
            log.warn("Unable to encrypt string: " + e);
            return null;
        }
    }

    /**
     * Decrypts a string that was previously {@link #encrypt(String)} encrypted}.
     *
     * @param s the data to decrypt
     * @return the string or {@code null} if an internal error occurred
     */
    private static String decrypt(String s) {
        if (s == null || !s.startsWith(PREFIX)) {
            return s;
        }
        try {
            byte[] data = new byte[(s.length() - PREFIX.length())/2];
            for (int i=PREFIX.length(),b=0; i<s.length(); i+=2, b++) {
                data[b] = (byte) (Integer.parseInt(s.substring(i, i+2), 16) &0xff);
            }
            SecretKeySpec key = new SecretKeySpec(data, 0, KEY_LENGTH, "DES");
            Cipher cipher = Cipher.getInstance("DES");
            try (ByteArrayOutputStream out = new ByteArrayOutputStream(data.length)) {
                cipher.init(Cipher.DECRYPT_MODE, key);
                out.write(cipher.update(data, KEY_LENGTH, data.length - KEY_LENGTH));
                out.write(cipher.doFinal());
                return out.toString("utf-8");
            }
        } catch (Exception e) {
            log.warn("Unable to decrypt data: " + e);
            return null;
        }
    }

}