/*
 * Copyright 2020 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.app.blob;

import static android.app.blob.XmlTags.ATTR_ALGO;
import static android.app.blob.XmlTags.ATTR_DIGEST;
import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME;
import static android.app.blob.XmlTags.ATTR_LABEL;
import static android.app.blob.XmlTags.ATTR_TAG;

import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.IndentingPrintWriter;

import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

/**
 * An identifier to represent a blob.
 */
// TODO: use datagen tool?
public final class BlobHandle implements Parcelable {
    /** @hide */
    public static final String ALGO_SHA_256 = "SHA-256";

    private static final String[] SUPPORTED_ALGOS = {
            ALGO_SHA_256
    };

    private static final int LIMIT_BLOB_TAG_LENGTH = 128; // characters
    private static final int LIMIT_BLOB_LABEL_LENGTH = 100; // characters

    /**
     * Cyrptographically secure hash algorithm used to generate hash of the blob this handle is
     * representing.
     *
     * @hide
     */
    @NonNull public final String algorithm;

    /**
     * Hash of the blob this handle is representing using {@link #algorithm}.
     *
     * @hide
     */
    @NonNull public final byte[] digest;

    /**
     * Label of the blob that can be surfaced to the user.
     * @hide
     */
    @NonNull public final CharSequence label;

    /**
     * Time in milliseconds after which the blob should be invalidated and not
     * allowed to be accessed by any other app, in {@link System#currentTimeMillis()} timebase.
     *
     * @hide
     */
    @CurrentTimeMillisLong public final long expiryTimeMillis;

    /**
     * An opaque {@link String} associated with the blob.
     *
     * @hide
     */
    @NonNull public final String tag;

    private BlobHandle(String algorithm, byte[] digest, CharSequence label, long expiryTimeMillis,
            String tag) {
        this.algorithm = algorithm;
        this.digest = digest;
        this.label = label;
        this.expiryTimeMillis = expiryTimeMillis;
        this.tag = tag;
    }

    private BlobHandle(Parcel in) {
        this.algorithm = in.readString();
        this.digest = in.createByteArray();
        this.label = in.readCharSequence();
        this.expiryTimeMillis = in.readLong();
        this.tag = in.readString();
    }

    /** @hide */
    public static @NonNull BlobHandle create(@NonNull String algorithm, @NonNull byte[] digest,
            @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis,
            @NonNull String tag) {
        final BlobHandle handle = new BlobHandle(algorithm, digest, label, expiryTimeMillis, tag);
        handle.assertIsValid();
        return handle;
    }

    /**
     * Create a new blob identifier.
     *
     * <p> For two objects of {@link BlobHandle} to be considered equal, the following arguments
     * must be equal:
     * <ul>
     * <li> {@code digest}
     * <li> {@code label}
     * <li> {@code expiryTimeMillis}
     * <li> {@code tag}
     * </ul>
     *
     * @param digest the SHA-256 hash of the blob this is representing.
     * @param label a label indicating what the blob is, that can be surfaced to the user.
     *              The length of the label cannot be more than 100 characters. It is recommended
     *              to keep this brief. This may be truncated and ellipsized if it is too long
     *              to be displayed to the user.
     * @param expiryTimeMillis the time in secs after which the blob should be invalidated and not
     *                         allowed to be accessed by any other app,
     *                         in {@link System#currentTimeMillis()} timebase or {@code 0} to
     *                         indicate that there is no expiry time associated with this blob.
     * @param tag an opaque {@link String} associated with the blob. The length of the tag
     *            cannot be more than 128 characters.
     *
     * @return a new instance of {@link BlobHandle} object.
     */
    public static @NonNull BlobHandle createWithSha256(@NonNull byte[] digest,
            @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis,
            @NonNull String tag) {
        return create(ALGO_SHA_256, digest, label, expiryTimeMillis, tag);
    }

    /**
     * Returns the SHA-256 hash of the blob that this object is representing.
     *
     * @see #createWithSha256(byte[], CharSequence, long, String)
     */
    public @NonNull byte[] getSha256Digest() {
        return digest;
    }

    /**
     * Returns the label associated with the blob that this object is representing.
     *
     * @see #createWithSha256(byte[], CharSequence, long, String)
     */
    public @NonNull CharSequence getLabel() {
        return label;
    }

    /**
     * Returns the expiry time in milliseconds of the blob that this object is representing, in
     *         {@link System#currentTimeMillis()} timebase.
     *
     * @see #createWithSha256(byte[], CharSequence, long, String)
     */
    public @CurrentTimeMillisLong long getExpiryTimeMillis() {
        return expiryTimeMillis;
    }

    /**
     * Returns the opaque {@link String} associated with the blob this object is representing.
     *
     * @see #createWithSha256(byte[], CharSequence, long, String)
     */
    public @NonNull String getTag() {
        return tag;
    }

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

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeString(algorithm);
        dest.writeByteArray(digest);
        dest.writeCharSequence(label);
        dest.writeLong(expiryTimeMillis);
        dest.writeString(tag);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || !(obj instanceof BlobHandle)) {
            return false;
        }
        final BlobHandle other = (BlobHandle) obj;
        return this.algorithm.equals(other.algorithm)
                && Arrays.equals(this.digest, other.digest)
                && this.label.toString().equals(other.label.toString())
                && this.expiryTimeMillis == other.expiryTimeMillis
                && this.tag.equals(other.tag);
    }

    @Override
    public int hashCode() {
        return Objects.hash(algorithm, Arrays.hashCode(digest), label, expiryTimeMillis, tag);
    }

    /** @hide */
    public void dump(IndentingPrintWriter fout, boolean dumpFull) {
        if (dumpFull) {
            fout.println("algo: " + algorithm);
            fout.println("digest: " + (dumpFull ? encodeDigest(digest) : safeDigest(digest)));
            fout.println("label: " + label);
            fout.println("expiryMs: " + expiryTimeMillis);
            fout.println("tag: " + tag);
        } else {
            fout.println(toString());
        }
    }

    /** @hide */
    public void assertIsValid() {
        Preconditions.checkArgumentIsSupported(SUPPORTED_ALGOS, algorithm);
        Preconditions.checkByteArrayNotEmpty(digest, "digest");
        Preconditions.checkStringNotEmpty(label, "label must not be null");
        Preconditions.checkArgument(label.length() <= LIMIT_BLOB_LABEL_LENGTH, "label too long");
        Preconditions.checkArgumentNonnegative(expiryTimeMillis,
                "expiryTimeMillis must not be negative");
        Preconditions.checkStringNotEmpty(tag, "tag must not be null");
        Preconditions.checkArgument(tag.length() <= LIMIT_BLOB_TAG_LENGTH, "tag too long");
    }

    @Override
    public String toString() {
        return "BlobHandle {"
                + "algo:" + algorithm + ","
                + "digest:" + safeDigest(digest) + ","
                + "label:" + label + ","
                + "expiryMs:" + expiryTimeMillis + ","
                + "tag:" + tag
                + "}";
    }

    /** @hide */
    public static String safeDigest(@NonNull byte[] digest) {
        final String digestStr = encodeDigest(digest);
        return digestStr.substring(0, 2) + ".." + digestStr.substring(digestStr.length() - 2);
    }

    private static String encodeDigest(@NonNull byte[] digest) {
        return Base64.encodeToString(digest, Base64.NO_WRAP);
    }

    /** @hide */
    public boolean isExpired() {
        return expiryTimeMillis != 0 && expiryTimeMillis < System.currentTimeMillis();
    }

    public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
        @Override
        public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {
            return new BlobHandle(source);
        }

        @Override
        public @NonNull BlobHandle[] newArray(int size) {
            return new BlobHandle[size];
        }
    };

    /** @hide */
    public void writeToXml(@NonNull XmlSerializer out) throws IOException {
        XmlUtils.writeStringAttribute(out, ATTR_ALGO, algorithm);
        XmlUtils.writeByteArrayAttribute(out, ATTR_DIGEST, digest);
        XmlUtils.writeStringAttribute(out, ATTR_LABEL, label);
        XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis);
        XmlUtils.writeStringAttribute(out, ATTR_TAG, tag);
    }

    /** @hide */
    @NonNull
    public static BlobHandle createFromXml(@NonNull XmlPullParser in) throws IOException {
        final String algo = XmlUtils.readStringAttribute(in, ATTR_ALGO);
        final byte[] digest = XmlUtils.readByteArrayAttribute(in, ATTR_DIGEST);
        final CharSequence label = XmlUtils.readStringAttribute(in, ATTR_LABEL);
        final long expiryTimeMs = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME);
        final String tag = XmlUtils.readStringAttribute(in, ATTR_TAG);

        return BlobHandle.create(algo, digest, label, expiryTimeMs, tag);
    }
}
