/**
 * 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.xml.security.stax.impl.securityToken;

import org.apache.xml.security.binding.xmldsig.*;
import org.apache.xml.security.binding.xmldsig11.ECKeyValueType;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.stax.ext.*;
import org.apache.xml.security.stax.impl.util.IDGenerator;
import org.apache.xml.security.stax.securityToken.SecurityTokenConstants.TokenType;
import org.apache.xml.security.stax.securityToken.InboundSecurityToken;
import org.apache.xml.security.stax.securityToken.SecurityTokenConstants;
import org.apache.xml.security.stax.securityToken.SecurityTokenFactory;
import org.apache.xml.security.utils.RFC2253Parser;
import org.apache.xml.security.utils.UnsyncByteArrayInputStream;

import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Map;

/**
 * Factory to create SecurityToken Objects from keys in XML
 *
 */
public class SecurityTokenFactoryImpl extends SecurityTokenFactory {

    public SecurityTokenFactoryImpl() {
    }

    @Override
    public InboundSecurityToken getSecurityToken(KeyInfoType keyInfoType,
                                          SecurityTokenConstants.KeyUsage keyUsage,
                                          XMLSecurityProperties securityProperties,
                                          InboundSecurityContext inboundSecurityContext) throws XMLSecurityException {
        if (keyInfoType != null) {
            // X509Data
            final X509DataType x509DataType =
                    XMLSecurityUtils.getQNameType(keyInfoType.getContent(), XMLSecurityConstants.TAG_dsig_X509Data);
            if (x509DataType != null) {
                return getSecurityToken(x509DataType, securityProperties, inboundSecurityContext, keyUsage);
            }

            // KeyValue
            final KeyValueType keyValueType
                    = XMLSecurityUtils.getQNameType(keyInfoType.getContent(), XMLSecurityConstants.TAG_dsig_KeyValue);
            if (keyValueType != null) {
                return getSecurityToken(keyValueType, securityProperties, inboundSecurityContext, keyUsage);
            }

            // KeyName
            final String keyName =
                    XMLSecurityUtils.getQNameType(keyInfoType.getContent(), XMLSecurityConstants.TAG_dsig_KeyName);
            if (keyName != null) {
                KeyNameSecurityToken token = getSecurityToken(keyName, securityProperties, inboundSecurityContext, keyUsage);
                return token;
            }
        }

        // Use a default key if it exists
        if (SecurityTokenConstants.KeyUsage_Signature_Verification.equals(keyUsage)
                && securityProperties.getSignatureVerificationKey() != null) {
            return getDefaultSecurityToken(securityProperties, inboundSecurityContext, keyUsage);
        } else if (SecurityTokenConstants.KeyUsage_Decryption.equals(keyUsage)
                && securityProperties.getDecryptionKey() != null) {
            return getDefaultSecurityToken(securityProperties, inboundSecurityContext, keyUsage);
        }

        throw new XMLSecurityException("stax.noKey", new Object[] {keyUsage});
    }

    private InboundSecurityToken getDefaultSecurityToken(XMLSecurityProperties securityProperties, final InboundSecurityContext inboundSecurityContext, SecurityTokenConstants.KeyUsage keyUsage) {
        AbstractInboundSecurityToken token =
                new AbstractInboundSecurityToken(inboundSecurityContext, IDGenerator.generateID(null),
                        SecurityTokenConstants.KeyIdentifier_NoKeyInfo, false) {
                    @Override
                    public TokenType getTokenType() {
                        return SecurityTokenConstants.DefaultToken;
                    }
                };
        setTokenKey(securityProperties, keyUsage, token);
        return token;
    }

    private KeyNameSecurityToken getSecurityToken(String keyName, XMLSecurityProperties securityProperties, InboundSecurityContext inboundSecurityContext,
            SecurityTokenConstants.KeyUsage keyUsage) throws XMLSecurityException {
        KeyNameSecurityToken token =
                new KeyNameSecurityToken(keyName, inboundSecurityContext);

        // This if is here to preserve the current behaviour when the SignatureVerificationKey is set
        if (SecurityTokenConstants.KeyUsage_Signature_Verification.equals(keyUsage)
                && securityProperties.getSignatureVerificationKey() == null) {
            Map<String, Key> keyNameMap = securityProperties.getKeyNameMap();
            Key key = keyNameMap.get(keyName);
            if (key == null) {
                throw new XMLSecurityException("stax.keyNotFoundForName", new Object[] {keyName});
            }

            if (key instanceof PublicKey) {
                token.setPublicKey((PublicKey)key);
            } else {
                throw new XMLSecurityException("stax.keyTypeNotSupported", new Object[] {key.getClass().getSimpleName()});
            }
        }

        setTokenKey(securityProperties, keyUsage, token);
        return token;
    }

    private static InboundSecurityToken getSecurityToken(KeyValueType keyValueType,
            XMLSecurityProperties securityProperties,
            InboundSecurityContext inboundSecurityContext,
            SecurityTokenConstants.KeyUsage keyUsage)
            throws XMLSecurityException {

        final RSAKeyValueType rsaKeyValueType =
                XMLSecurityUtils.getQNameType(keyValueType.getContent(), XMLSecurityConstants.TAG_dsig_RSAKeyValue);
        if (rsaKeyValueType != null) {
            RsaKeyValueSecurityToken token =
                new RsaKeyValueSecurityToken(rsaKeyValueType, inboundSecurityContext);
            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }
        final DSAKeyValueType dsaKeyValueType =
                XMLSecurityUtils.getQNameType(keyValueType.getContent(), XMLSecurityConstants.TAG_dsig_DSAKeyValue);
        if (dsaKeyValueType != null) {
            DsaKeyValueSecurityToken token =
                    new DsaKeyValueSecurityToken(dsaKeyValueType, inboundSecurityContext);
            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }
        final ECKeyValueType ecKeyValueType =
                XMLSecurityUtils.getQNameType(keyValueType.getContent(), XMLSecurityConstants.TAG_dsig11_ECKeyValue);
        if (ecKeyValueType != null) {
            ECKeyValueSecurityToken token =
                    new ECKeyValueSecurityToken(ecKeyValueType, inboundSecurityContext);
            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }
        throw new XMLSecurityException("stax.unsupportedKeyValue");
    }

    private static InboundSecurityToken getSecurityToken(X509DataType x509DataType,
                                                  XMLSecurityProperties securityProperties,
                                                  InboundSecurityContext inboundSecurityContext,
                                                  SecurityTokenConstants.KeyUsage keyUsage)
            throws XMLSecurityException {
        // X509Certificate
        byte[] certBytes =
                XMLSecurityUtils.getQNameType(
                        x509DataType.getX509IssuerSerialOrX509SKIOrX509SubjectName(),
                        XMLSecurityConstants.TAG_dsig_X509Certificate
                );
        if (certBytes != null) {
            X509Certificate cert = getCertificateFromBytes(certBytes);
            TokenType tokenType = SecurityTokenConstants.X509V3Token;
            if (cert.getVersion() == 1) {
                tokenType = SecurityTokenConstants.X509V1Token;
            }
            X509SecurityToken token =
                    new X509SecurityToken(tokenType, inboundSecurityContext,
                            IDGenerator.generateID(null), SecurityTokenConstants.KeyIdentifier_X509KeyIdentifier, true);
            token.setX509Certificates(new X509Certificate[]{cert});

            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }

        // Issuer Serial
        final X509IssuerSerialType issuerSerialType =
                XMLSecurityUtils.getQNameType(
                        x509DataType.getX509IssuerSerialOrX509SKIOrX509SubjectName(),
                        XMLSecurityConstants.TAG_dsig_X509IssuerSerial
                );
        if (issuerSerialType != null) {
            if (issuerSerialType.getX509IssuerName() == null
                    || issuerSerialType.getX509SerialNumber() == null
                    || SecurityTokenConstants.KeyUsage_Signature_Verification.equals(keyUsage)
                        && securityProperties.getSignatureVerificationKey() == null
                    || SecurityTokenConstants.KeyUsage_Decryption.equals(keyUsage)
                        && securityProperties.getDecryptionKey() == null) {
                throw new XMLSecurityException("stax.noKey", new Object[] {keyUsage});
            }
            X509IssuerSerialSecurityToken token =
                    new X509IssuerSerialSecurityToken(
                            SecurityTokenConstants.X509V3Token, inboundSecurityContext, IDGenerator.generateID(null));
            token.setIssuerName(issuerSerialType.getX509IssuerName());
            token.setSerialNumber(issuerSerialType.getX509SerialNumber());

            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }

        // Subject Key Identifier
        byte[] skiBytes =
                XMLSecurityUtils.getQNameType(
                        x509DataType.getX509IssuerSerialOrX509SKIOrX509SubjectName(),
                        XMLSecurityConstants.TAG_dsig_X509SKI
                );
        if (skiBytes != null) {
            if (SecurityTokenConstants.KeyUsage_Signature_Verification.equals(keyUsage)
                && securityProperties.getSignatureVerificationKey() == null
                || SecurityTokenConstants.KeyUsage_Decryption.equals(keyUsage)
                && securityProperties.getDecryptionKey() == null) {
                throw new XMLSecurityException("stax.noKey", new Object[] {keyUsage});
            }
            X509SKISecurityToken token =
                    new X509SKISecurityToken(
                            SecurityTokenConstants.X509V3Token, inboundSecurityContext, IDGenerator.generateID(null));
            token.setSkiBytes(skiBytes);

            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }

        // Subject Name
        String subjectName =
                XMLSecurityUtils.getQNameType(
                        x509DataType.getX509IssuerSerialOrX509SKIOrX509SubjectName(),
                        XMLSecurityConstants.TAG_dsig_X509SubjectName
                );
        if (subjectName != null) {
            if (SecurityTokenConstants.KeyUsage_Signature_Verification.equals(keyUsage)
                && securityProperties.getSignatureVerificationKey() == null
                || SecurityTokenConstants.KeyUsage_Decryption.equals(keyUsage)
                && securityProperties.getDecryptionKey() == null) {
                throw new XMLSecurityException("stax.noKey", new Object[] {keyUsage});
            }
            String normalizedSubjectName =
                    RFC2253Parser.normalize(subjectName);
            X509SubjectNameSecurityToken token =
                    new X509SubjectNameSecurityToken(
                            SecurityTokenConstants.X509V3Token, inboundSecurityContext, IDGenerator.generateID(null));
            token.setSubjectName(normalizedSubjectName);

            setTokenKey(securityProperties, keyUsage, token);
            return token;
        }

        throw new XMLSecurityException("stax.noKey", new Object[] {keyUsage});
    }

    private static void setTokenKey(XMLSecurityProperties securityProperties, SecurityTokenConstants.KeyUsage keyUsage,
                                    AbstractInboundSecurityToken token) {
        Key key = null;
        if (SecurityTokenConstants.KeyUsage_Signature_Verification.equals(keyUsage)) {
            key = securityProperties.getSignatureVerificationKey();
        } else if (SecurityTokenConstants.KeyUsage_Decryption.equals(keyUsage)) {
            key = securityProperties.getDecryptionKey();
        }
        if (key instanceof PublicKey
            && !SecurityTokenConstants.KeyValueToken.equals(token.getTokenType())) {
            token.setPublicKey((PublicKey) key);
        } else {
            token.setSecretKey("", key);
        }
    }

    /**
     * Construct an X509Certificate'from the byte array.
     * <p></p>
     *
     * @param data The <code>byte</code> array containing the X509 data
     * @return An X509 certificate
     * @throws XMLSecurityException
     */
    private static X509Certificate getCertificateFromBytes(byte[] data)
            throws XMLSecurityException {
        try (InputStream in = new UnsyncByteArrayInputStream(data)) {
            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            return (X509Certificate) factory.generateCertificate(in);
        } catch (CertificateException | IOException e) {
            throw new XMLSecurityException(e);
        }
    }
}
