/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed 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 com.android.server.locksettings.recoverablekeystore.certificate;

import android.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;

import java.security.SecureRandom;
import java.security.cert.CertPath;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import org.w3c.dom.Element;

/**
 * Parses and holds the XML file containing the list of THM public-key certificates and related
 * metadata.
 */
public final class CertXml {

    private static final String METADATA_NODE_TAG = "metadata";
    private static final String METADATA_SERIAL_NODE_TAG = "serial";
    private static final String METADATA_REFRESH_INTERVAL_NODE_TAG = "refresh-interval";
    private static final String ENDPOINT_CERT_LIST_TAG = "endpoints";
    private static final String ENDPOINT_CERT_ITEM_TAG = "cert";
    private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
    private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";

    private final long serial;
    private final long refreshInterval;
    private final List<X509Certificate> intermediateCerts;
    private final List<X509Certificate> endpointCerts;

    private CertXml(
            long serial,
            long refreshInterval,
            List<X509Certificate> intermediateCerts,
            List<X509Certificate> endpointCerts) {
        this.serial = serial;
        this.refreshInterval = refreshInterval;
        this.intermediateCerts = intermediateCerts;
        this.endpointCerts = endpointCerts;
    }

    /** Gets the serial number of the XML file containing public-key certificates. */
    public long getSerial() {
        return serial;
    }

    /**
     * Gets the refresh interval in the XML file containing public-key certificates. The refresh
     * interval denotes the number of seconds that the client should follow to contact the server to
     * refresh the XML file.
     */
    public long getRefreshInterval() {
        return refreshInterval;
    }

    @VisibleForTesting
    List<X509Certificate> getAllIntermediateCerts() {
        return intermediateCerts;
    }

    @VisibleForTesting
    List<X509Certificate> getAllEndpointCerts() {
        return endpointCerts;
    }

    /**
     * Chooses a random endpoint certificate from the XML file, validates the chosen certificate,
     * and returns the certificate path including the chosen certificate if it is valid.
     *
     * @param trustedRoot the trusted root certificate
     * @return the certificate path including the chosen certificate if the certificate is valid
     * @throws CertValidationException if the chosen certificate cannot be validated based on the
     *                                 trusted root certificate
     */
    public CertPath getRandomEndpointCert(X509Certificate trustedRoot)
            throws CertValidationException {
        return getEndpointCert(
                new SecureRandom().nextInt(this.endpointCerts.size()),
                /*validationDate=*/ null,
                trustedRoot);
    }

    @VisibleForTesting
    CertPath getEndpointCert(
            int index, @Nullable Date validationDate, X509Certificate trustedRoot)
            throws CertValidationException {
        X509Certificate chosenCert = endpointCerts.get(index);
        return CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, chosenCert);
    }

    /**
     * Parses a byte array as the content of the XML file containing a list of endpoint
     * certificates.
     *
     * @param bytes the bytes of the XML file
     * @return a {@code CertXml} instance that contains the parsing result
     * @throws CertParsingException if any parsing error occurs
     */
    public static CertXml parse(byte[] bytes) throws CertParsingException {
        Element rootNode = CertUtils.getXmlRootNode(bytes);
        return new CertXml(
                parseSerial(rootNode),
                parseRefreshInterval(rootNode),
                parseIntermediateCerts(rootNode),
                parseEndpointCerts(rootNode));
    }

    private static long parseSerial(Element rootNode) throws CertParsingException {
        List<String> contents =
                CertUtils.getXmlNodeContents(
                        CertUtils.MUST_EXIST_EXACTLY_ONE,
                        rootNode,
                        METADATA_NODE_TAG,
                        METADATA_SERIAL_NODE_TAG);
        return Long.parseLong(contents.get(0));
    }

    private static long parseRefreshInterval(Element rootNode) throws CertParsingException {
        List<String> contents =
                CertUtils.getXmlNodeContents(
                        CertUtils.MUST_EXIST_EXACTLY_ONE,
                        rootNode,
                        METADATA_NODE_TAG,
                        METADATA_REFRESH_INTERVAL_NODE_TAG);
        return Long.parseLong(contents.get(0));
    }

    private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
            throws CertParsingException {
        List<String> contents =
                CertUtils.getXmlNodeContents(
                        CertUtils.MUST_EXIST_UNENFORCED,
                        rootNode,
                        INTERMEDIATE_CERT_LIST_TAG,
                        INTERMEDIATE_CERT_ITEM_TAG);
        List<X509Certificate> res = new ArrayList<>();
        for (String content : contents) {
            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
        }
        return Collections.unmodifiableList(res);
    }

    private static List<X509Certificate> parseEndpointCerts(Element rootNode)
            throws CertParsingException {
        List<String> contents =
                CertUtils.getXmlNodeContents(
                        CertUtils.MUST_EXIST_AT_LEAST_ONE,
                        rootNode,
                        ENDPOINT_CERT_LIST_TAG,
                        ENDPOINT_CERT_ITEM_TAG);
        List<X509Certificate> res = new ArrayList<>();
        for (String content : contents) {
            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
        }
        return Collections.unmodifiableList(res);
    }
}
