/*
 * 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 android.security.keystore.recovery;

import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.BadParcelableException;
import android.os.Parcel;
import android.os.Parcelable;

import com.android.internal.util.Preconditions;

import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Objects;

/**
 * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot:
 *
 * <ul>
 *     <li>The user's lock screen changes. (A key derived from the user's lock screen is used to
 *         protected the keychain, which is why this forces a new snapshot.)
 *     <li>A key is added to or removed from the recoverable keychain.
 * </ul>
 *
 * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even
 * the recovery agent itself should not be able to decipher the data. The recovery agent sends an
 * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a
 * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then
 * sends it to the framework, where it is decrypted using the user's lock screen from their previous
 * device.
 *
 * @hide
 */
@SystemApi
public final class KeyChainSnapshot implements Parcelable {

    // IMPORTANT! PLEASE READ!
    // -----------------------
    // If you edit this file (e.g., to add new fields), please MAKE SURE to also do the following:
    // - Update the #writeToParcel(Parcel) method below
    // - Update the #(Parcel) constructor below
    // - Update android.security.keystore.recovery.KeyChainSnapshotTest to make sure nobody
    //     accidentally breaks your fields in the Parcel in the future.
    // - Update com.android.server.locksettings.recoverablekeystore.serialization
    //     .KeyChainSnapshotSerializer to correctly serialize your new field
    // - Update com.android.server.locksettings.recoverablekeystore.serialization
    //     .KeyChainSnapshotSerializer to correctly deserialize your new field
    // - Update com.android.server.locksettings.recoverablekeystore.serialization
    //     .KeychainSnapshotSerializerTest to make sure nobody breaks serialization of your field
    //     in the future.

    private static final int DEFAULT_MAX_ATTEMPTS = 10;
    private static final long DEFAULT_COUNTER_ID = 1L;

    private int mSnapshotVersion;
    private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
    private long mCounterId = DEFAULT_COUNTER_ID;
    private byte[] mServerParams;
    private RecoveryCertPath mCertPath;  // The cert path including necessary intermediate certs
    private List<KeyChainProtectionParams> mKeyChainProtectionParams;
    private List<WrappedApplicationKey> mEntryRecoveryData;
    private byte[] mEncryptedRecoveryKeyBlob;

    /**
     * Use builder to create an instance of the class.
     */
    private KeyChainSnapshot() {

    }

    /**
     * Snapshot version for given recovery agent. It is incremented when user secret or list of
     * application keys changes.
     */
    public int getSnapshotVersion() {
        return mSnapshotVersion;
    }

    /**
     * Number of user secret guesses allowed during KeyChain recovery.
     */
    public int getMaxAttempts() {
        return mMaxAttempts;
    }

    /**
     * CounterId which is rotated together with user secret.
     */
    public long getCounterId() {
        return mCounterId;
    }

    /**
     * Server parameters.
     */
    public @NonNull byte[] getServerParams() {
        return mServerParams;
    }

    /**
     * CertPath containing the public key used to encrypt {@code encryptedRecoveryKeyBlob}.
     */
    public @NonNull CertPath getTrustedHardwareCertPath() {
        try {
            return mCertPath.getCertPath();
        } catch (CertificateException e) {
            // Rethrow an unchecked exception as it should not happen. If such an issue exists,
            // an exception should have been thrown during service initialization.
            throw new BadParcelableException(e);
        }
    }

    /**
     * UI and key derivation parameters. Note that combination of secrets may be used.
     */
    public @NonNull List<KeyChainProtectionParams> getKeyChainProtectionParams() {
        return mKeyChainProtectionParams;
    }

    /**
     * List of application keys, with key material encrypted by
     * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
     */
    public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
        return mEntryRecoveryData;
    }

    /**
     * Recovery key blob, encrypted by user secret and recovery service public key.
     */
    public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
        return mEncryptedRecoveryKeyBlob;
    }

    public static final @NonNull Creator<KeyChainSnapshot> CREATOR =
            new Creator<KeyChainSnapshot>() {
        public KeyChainSnapshot createFromParcel(Parcel in) {
            return new KeyChainSnapshot(in);
        }

        public KeyChainSnapshot[] newArray(int length) {
            return new KeyChainSnapshot[length];
        }
    };

    /**
     * Builder for creating {@link KeyChainSnapshot}.
     * @hide
     */
    public static class Builder {
        private KeyChainSnapshot mInstance = new KeyChainSnapshot();

        /**
         * Snapshot version for the recovery agent.
         *
         * @param snapshotVersion The snapshot version
         * @return This builder.
         */
        public @NonNull Builder setSnapshotVersion(int snapshotVersion) {
            mInstance.mSnapshotVersion = snapshotVersion;
            return this;
        }

        /**
         * Sets the number of user secret guesses allowed during Keychain recovery.
         *
         * @param maxAttempts The maximum number of guesses.
         * @return This builder.
         */
        public @NonNull Builder setMaxAttempts(int maxAttempts) {
            mInstance.mMaxAttempts = maxAttempts;
            return this;
        }

        /**
         * Sets counter id.
         *
         * @param counterId The counter id.
         * @return This builder.
         */
        public @NonNull Builder setCounterId(long counterId) {
            mInstance.mCounterId = counterId;
            return this;
        }

        /**
         * Sets server parameters.
         *
         * @param serverParams The server parameters
         * @return This builder.
         */
        public @NonNull Builder setServerParams(byte[] serverParams) {
            mInstance.mServerParams = serverParams;
            return this;
        }

        /**
         * Sets CertPath used to validate the trusted hardware public key. The CertPath should
         * contain a certificate of the trusted hardware public key and any necessary intermediate
         * certificates.
         *
         * @param certPath The certificate path
         * @throws CertificateException if the given certificate path cannot be encoded properly
         * @return This builder.
         */
        public @NonNull Builder setTrustedHardwareCertPath(@NonNull CertPath certPath)
                throws CertificateException {
            mInstance.mCertPath = RecoveryCertPath.createRecoveryCertPath(certPath);
            return this;
        }

        /**
         * Sets UI and key derivation parameters
         *
         * @param keyChainProtectionParams The UI and key derivation parameters
         * @return This builder.
         */
        public @NonNull Builder setKeyChainProtectionParams(
                @NonNull List<KeyChainProtectionParams> keyChainProtectionParams) {
            mInstance.mKeyChainProtectionParams = keyChainProtectionParams;
            return this;
        }

        /**
         * List of application keys.
         *
         * @param entryRecoveryData List of application keys
         * @return This builder.
         */
        public @NonNull Builder setWrappedApplicationKeys(
                @NonNull List<WrappedApplicationKey> entryRecoveryData) {
            mInstance.mEntryRecoveryData = entryRecoveryData;
            return this;
        }

        /**
         * Sets recovery key blob.
         *
         * @param encryptedRecoveryKeyBlob The recovery key blob.
         * @return This builder.
         */
        public @NonNull Builder setEncryptedRecoveryKeyBlob(
                @NonNull byte[] encryptedRecoveryKeyBlob) {
            mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
            return this;
        }


        /**
         * Creates a new {@link KeyChainSnapshot} instance.
         *
         * @return new instance
         * @throws NullPointerException if some of the required fields were not set.
         */
        public @NonNull KeyChainSnapshot build() {
            Preconditions.checkCollectionElementsNotNull(mInstance.mKeyChainProtectionParams,
                    "keyChainProtectionParams");
            Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
                    "entryRecoveryData");
            Objects.requireNonNull(mInstance.mEncryptedRecoveryKeyBlob);
            Objects.requireNonNull(mInstance.mServerParams);
            Objects.requireNonNull(mInstance.mCertPath);
            return mInstance;
        }
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mSnapshotVersion);
        out.writeTypedList(mKeyChainProtectionParams);
        out.writeByteArray(mEncryptedRecoveryKeyBlob);
        out.writeTypedList(mEntryRecoveryData);
        out.writeInt(mMaxAttempts);
        out.writeLong(mCounterId);
        out.writeByteArray(mServerParams);
        out.writeTypedObject(mCertPath, /* no flags */ 0);
    }

    /**
     * @hide
     */
    protected KeyChainSnapshot(Parcel in) {
        mSnapshotVersion = in.readInt();
        mKeyChainProtectionParams = in.createTypedArrayList(KeyChainProtectionParams.CREATOR);
        mEncryptedRecoveryKeyBlob = in.createByteArray();
        mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
        mMaxAttempts = in.readInt();
        mCounterId = in.readLong();
        mServerParams = in.createByteArray();
        mCertPath = in.readTypedObject(RecoveryCertPath.CREATOR);
    }

    @Override
    public int describeContents() {
        return 0;
    }
}
