/*
 * Copyright (C) 2018 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.serialization;

import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.CERTIFICATE_FACTORY_TYPE;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.NAMESPACE;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_ALGORITHM;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_ALIAS;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_APPLICATION_KEY;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_APPLICATION_KEYS;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_BACKEND_PUBLIC_KEY;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_COUNTER_ID;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_CHAIN_PROTECTION_PARAMS;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_CHAIN_SNAPSHOT;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_DERIVATION_PARAMS;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_MATERIAL;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_METADATA;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_LOCK_SCREEN_UI_TYPE;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_MAX_ATTEMPTS;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_MEMORY_DIFFICULTY;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_RECOVERY_KEY_MATERIAL;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_SALT;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_SERVER_PARAMS;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_SNAPSHOT_VERSION;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_TRUSTED_HARDWARE_CERT_PATH;
import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_USER_SECRET_TYPE;

import android.security.keystore.recovery.KeyChainProtectionParams;
import android.security.keystore.recovery.KeyChainSnapshot;
import android.security.keystore.recovery.KeyDerivationParams;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.util.Base64;
import android.util.TypedXmlPullParser;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Deserializes a {@link android.security.keystore.recovery.KeyChainSnapshot} instance from XML.
 */
public class KeyChainSnapshotDeserializer {

    /**
     * Deserializes a {@link KeyChainSnapshot} instance from the XML in the {@code inputStream}.
     *
     * @throws IOException if there is an IO error reading from the stream.
     * @throws KeyChainSnapshotParserException if the XML does not conform to the expected XML for
     *     a snapshot.
     */
    public static KeyChainSnapshot deserialize(InputStream inputStream)
            throws KeyChainSnapshotParserException, IOException {
        try {
            return deserializeInternal(inputStream);
        } catch (XmlPullParserException e) {
            throw new KeyChainSnapshotParserException("Malformed KeyChainSnapshot XML", e);
        }
    }

    private static KeyChainSnapshot deserializeInternal(InputStream inputStream) throws IOException,
            XmlPullParserException, KeyChainSnapshotParserException {
        TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);

        parser.nextTag();
        parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_SNAPSHOT);

        KeyChainSnapshot.Builder builder = new KeyChainSnapshot.Builder();
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }

            String name = parser.getName();

            switch (name) {
                case TAG_SNAPSHOT_VERSION:
                    builder.setSnapshotVersion(readIntTag(parser, TAG_SNAPSHOT_VERSION));
                    break;

                case TAG_RECOVERY_KEY_MATERIAL:
                    builder.setEncryptedRecoveryKeyBlob(
                            readBlobTag(parser, TAG_RECOVERY_KEY_MATERIAL));
                    break;

                case TAG_COUNTER_ID:
                    builder.setCounterId(readLongTag(parser, TAG_COUNTER_ID));
                    break;

                case TAG_SERVER_PARAMS:
                    builder.setServerParams(readBlobTag(parser, TAG_SERVER_PARAMS));
                    break;

                case TAG_MAX_ATTEMPTS:
                    builder.setMaxAttempts(readIntTag(parser, TAG_MAX_ATTEMPTS));
                    break;

                case TAG_TRUSTED_HARDWARE_CERT_PATH:
                    try {
                        builder.setTrustedHardwareCertPath(
                                readCertPathTag(parser, TAG_TRUSTED_HARDWARE_CERT_PATH));
                    } catch (CertificateException e) {
                        throw new KeyChainSnapshotParserException(
                                "Could not set trustedHardwareCertPath", e);
                    }
                    break;

                case TAG_BACKEND_PUBLIC_KEY:
                    // Unused
                    break;

                case TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST:
                    builder.setKeyChainProtectionParams(readKeyChainProtectionParamsList(parser));
                    break;

                case TAG_APPLICATION_KEYS:
                    builder.setWrappedApplicationKeys(readWrappedApplicationKeys(parser));
                    break;

                default:
                    throw new KeyChainSnapshotParserException(String.format(
                            Locale.US, "Unexpected tag %s in keyChainSnapshot", name));
            }
        }

        parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_CHAIN_SNAPSHOT);
        try {
            return builder.build();
        } catch (NullPointerException e) {
            throw new KeyChainSnapshotParserException("Failed to build KeyChainSnapshot", e);
        }
    }

    private static List<WrappedApplicationKey> readWrappedApplicationKeys(TypedXmlPullParser parser)
            throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_APPLICATION_KEYS);
        ArrayList<WrappedApplicationKey> keys = new ArrayList<>();
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            keys.add(readWrappedApplicationKey(parser));
        }
        parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_APPLICATION_KEYS);
        return keys;
    }

    private static WrappedApplicationKey readWrappedApplicationKey(TypedXmlPullParser parser)
            throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_APPLICATION_KEY);
        WrappedApplicationKey.Builder builder = new WrappedApplicationKey.Builder();
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }

            String name = parser.getName();

            switch (name) {
                case TAG_ALIAS:
                    builder.setAlias(readStringTag(parser, TAG_ALIAS));
                    break;

                case TAG_KEY_MATERIAL:
                    builder.setEncryptedKeyMaterial(readBlobTag(parser, TAG_KEY_MATERIAL));
                    break;

                case TAG_KEY_METADATA:
                    builder.setMetadata(readBlobTag(parser, TAG_KEY_METADATA));
                    break;

                default:
                    throw new KeyChainSnapshotParserException(String.format(
                            Locale.US, "Unexpected tag %s in wrappedApplicationKey", name));
            }
        }
        parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_APPLICATION_KEY);

        try {
            return builder.build();
        } catch (NullPointerException e) {
            throw new KeyChainSnapshotParserException("Failed to build WrappedApplicationKey", e);
        }
    }

    private static List<KeyChainProtectionParams> readKeyChainProtectionParamsList(
            TypedXmlPullParser parser) throws IOException, XmlPullParserException,
            KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST);

        ArrayList<KeyChainProtectionParams> keyChainProtectionParamsList = new ArrayList<>();
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            keyChainProtectionParamsList.add(readKeyChainProtectionParams(parser));
        }

        parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST);
        return keyChainProtectionParamsList;
    }

    private static KeyChainProtectionParams readKeyChainProtectionParams(TypedXmlPullParser parser)
        throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS);

        KeyChainProtectionParams.Builder builder = new KeyChainProtectionParams.Builder();
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }

            String name = parser.getName();

            switch (name) {
                case TAG_LOCK_SCREEN_UI_TYPE:
                    builder.setLockScreenUiFormat(readIntTag(parser, TAG_LOCK_SCREEN_UI_TYPE));
                    break;

                case TAG_USER_SECRET_TYPE:
                    builder.setUserSecretType(readIntTag(parser, TAG_USER_SECRET_TYPE));
                    break;

                case TAG_KEY_DERIVATION_PARAMS:
                    builder.setKeyDerivationParams(readKeyDerivationParams(parser));
                    break;

                default:
                    throw new KeyChainSnapshotParserException(String.format(
                            Locale.US,
                            "Unexpected tag %s in keyChainProtectionParams",
                            name));

            }
        }

        parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS);

        try {
            return builder.build();
        } catch (NullPointerException e) {
            throw new KeyChainSnapshotParserException(
                    "Failed to build KeyChainProtectionParams", e);
        }
    }

    private static KeyDerivationParams readKeyDerivationParams(TypedXmlPullParser parser)
            throws XmlPullParserException, IOException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_DERIVATION_PARAMS);

        int memoryDifficulty = -1;
        int algorithm = -1;
        byte[] salt = null;

        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }

            String name = parser.getName();

            switch (name) {
                case TAG_MEMORY_DIFFICULTY:
                    memoryDifficulty = readIntTag(parser, TAG_MEMORY_DIFFICULTY);
                    break;

                case TAG_ALGORITHM:
                    algorithm = readIntTag(parser, TAG_ALGORITHM);
                    break;

                case TAG_SALT:
                    salt = readBlobTag(parser, TAG_SALT);
                    break;

                default:
                    throw new KeyChainSnapshotParserException(
                            String.format(
                                    Locale.US,
                                    "Unexpected tag %s in keyDerivationParams",
                                    name));
            }
        }

        if (salt == null) {
            throw new KeyChainSnapshotParserException("salt was not set in keyDerivationParams");
        }

        KeyDerivationParams keyDerivationParams = null;

        switch (algorithm) {
            case KeyDerivationParams.ALGORITHM_SHA256:
                keyDerivationParams = KeyDerivationParams.createSha256Params(salt);
                break;

            case KeyDerivationParams.ALGORITHM_SCRYPT:
                keyDerivationParams = KeyDerivationParams.createScryptParams(
                        salt, memoryDifficulty);
                break;

            default:
                throw new KeyChainSnapshotParserException(
                        "Unknown algorithm in keyDerivationParams");
        }

        parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_DERIVATION_PARAMS);
        return keyDerivationParams;
    }

    private static int readIntTag(TypedXmlPullParser parser, String tagName)
            throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
        String text = readText(parser);
        parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
        try {
            return Integer.valueOf(text);
        } catch (NumberFormatException e) {
            throw new KeyChainSnapshotParserException(
                    String.format(
                            Locale.US, "%s expected int but got '%s'", tagName, text), e);
        }
    }

    private static long readLongTag(TypedXmlPullParser parser, String tagName)
            throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
        String text = readText(parser);
        parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
        try {
            return Long.valueOf(text);
        } catch (NumberFormatException e) {
            throw new KeyChainSnapshotParserException(
                    String.format(
                            Locale.US, "%s expected long but got '%s'", tagName, text), e);
        }
    }

    private static String readStringTag(TypedXmlPullParser parser, String tagName)
            throws IOException, XmlPullParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
        String text = readText(parser);
        parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
        return text;
    }

    private static byte[] readBlobTag(TypedXmlPullParser parser, String tagName)
            throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
        String text = readText(parser);
        parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);

        try {
            return Base64.decode(text, /*flags=*/ Base64.DEFAULT);
        } catch (IllegalArgumentException e) {
            throw new KeyChainSnapshotParserException(
                    String.format(
                            Locale.US,
                            "%s expected base64 encoded bytes but got '%s'",
                            tagName, text), e);
        }
    }

    private static CertPath readCertPathTag(TypedXmlPullParser parser, String tagName)
            throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
        byte[] bytes = readBlobTag(parser, tagName);
        try {
            return CertificateFactory.getInstance(CERTIFICATE_FACTORY_TYPE)
                    .generateCertPath(new ByteArrayInputStream(bytes));
        } catch (CertificateException e) {
            throw new KeyChainSnapshotParserException("Could not parse CertPath in tag " + tagName,
                    e);
        }
    }

    private static String readText(TypedXmlPullParser parser)
            throws IOException, XmlPullParserException {
        String result = "";
        if (parser.next() == XmlPullParser.TEXT) {
            result = parser.getText();
            parser.nextTag();
        }
        return result;
    }

    // Statics only
    private KeyChainSnapshotDeserializer() {}
}
