/*
 * Copyright 2019 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;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.os.IInstalld.IFsveritySetupAuthToken;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.system.ErrnoException;

import com.android.internal.security.VerityUtils;

import java.io.File;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;

/**
 * This class provides access to file integrity related operations.
 */
@SystemService(Context.FILE_INTEGRITY_SERVICE)
public final class FileIntegrityManager {
    @NonNull private final IFileIntegrityService mService;
    @NonNull private final Context mContext;

    /** @hide */
    public FileIntegrityManager(@NonNull Context context, @NonNull IFileIntegrityService service) {
        mContext = context;
        mService = service;
    }

    /**
     * Returns whether fs-verity is supported on the device. fs-verity provides on-access
     * verification, although the app APIs are only made available to apps in a later SDK version.
     * Only when this method returns true, the other fs-verity APIs in the same class can succeed.
     *
     * <p>The app may not need this method and just call the other APIs normally and handle any
     * failure. If some app feature really depends on fs-verity (e.g. protecting integrity of a
     * large file download), an early check of support status may avoid any cost if it is to fail
     * late.
     *
     * <p>Note: for historical reasons this is named {@code isApkVeritySupported()} instead of
     * {@code isFsVeritySupported()}. It has also been available since API level 30, predating the
     * other fs-verity APIs.
     */
    public boolean isApkVeritySupported() {
        return VerityUtils.isFsVeritySupported();
    }

    /**
     * Enables fs-verity to the owned file under the calling app's private directory. It always uses
     * the common configuration, i.e. SHA-256 digest algorithm, 4K block size, and without salt.
     *
     * <p>For enabling fs-verity to succeed, the device must support fs-verity, the file must be
     * writable by the app and not already have fs-verity enabled, and the file must not currently
     * be open for writing by any process. To check whether the device supports fs-verity, use
     * {@link #isApkVeritySupported()}.
     *
     * <p>It takes O(file size) time to build the underlying data structure for continuous
     * verification. The operation is atomic, i.e. it's either enabled or not, even in case of
     * power failure during or after the call.
     *
     * <p>Note for the API users: When the file's authenticity is crucial, the app typical needs to
     * perform a signature check by itself before using the file. The signature is often delivered
     * as a separate file and stored next to the targeting file in the filesystem. The public key of
     * the signer (normally the same app developer) can be put in the APK, and the app can use the
     * public key to verify the signature to the file's actual fs-verity digest (from {@link
     * #getFsVerityDigest(File)}) before using the file. The exact format is not prescribed by the
     * framework. App developers may choose to use common practices like JCA for the signing and
     * verification, or their own preferred approach.
     *
     * @param file The file to enable fs-verity. It must represent an absolute path.
     * @throws IllegalArgumentException If the provided file is not an absolute path.
     * @throws IOException If the operation failed.
     *
     * @see <a href="https://www.kernel.org/doc/html/next/filesystems/fsverity.html">Kernel doc</a>
     * @hide
     */
    @FlaggedApi(Flags.FLAG_FSVERITY_API)
    @SuppressLint("StreamFiles")
    @SystemApi
    public void setupFsVerity(@NonNull File file) throws IOException {
        if (!file.isAbsolute()) {
            // fs-verity is to be enabled by installd, which enforces the validation to the
            // (untrusted) file path passed from here. To make this less error prone, installd
            // accepts only absolute path. When a relative path is provided, we fail with an
            // explicit exception to help developers understand the requirement to use an absolute
            // path.
            throw new IllegalArgumentException("Expect an absolute path");
        }
        IFsveritySetupAuthToken authToken;
        // fs-verity setup requires no writable fd to the file. Make sure it's closed before
        // continue.
        try (var authFd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)) {
            authToken = mService.createAuthToken(authFd);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }

        try {
            int errno = mService.setupFsverity(authToken, file.getPath(),
                    mContext.getPackageName());
            if (errno != 0) {
                new ErrnoException("setupFsVerity", errno).rethrowAsIOException();
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Returns the fs-verity digest for the owned file under the calling app's private directory, or
     * null when the file does not have fs-verity enabled (including when fs-verity is not supported
     * on older devices).
     *
     * @param file The file to measure the fs-verity digest.
     * @return The fs-verity digest in byte[], null if none.
     * @see <a href="https://www.kernel.org/doc/html/next/filesystems/fsverity.html">Kernel doc</a>
     * @hide
     */
    @FlaggedApi(Flags.FLAG_FSVERITY_API)
    @SuppressLint("StreamFiles")
    @SystemApi
    public @Nullable byte[] getFsVerityDigest(@NonNull File file) throws IOException {
        return VerityUtils.getFsverityDigest(file.getPath());
    }

    /**
     * Returns whether the given certificate can be used to prove app's install source. Always
     * return false if the feature is not supported.
     *
     * <p>A store can use this API to decide if a signature file needs to be downloaded. Also, if a
     * store has shipped different certificates before (e.g. with stronger and weaker key), it can
     * also use this API to download the best signature on the running device.
     *
     * @return whether the certificate is trusted in the system
     * @deprecated The feature is no longer supported, and this API now always returns false.
     */
    @RequiresPermission(anyOf = {
            android.Manifest.permission.INSTALL_PACKAGES,
            android.Manifest.permission.REQUEST_INSTALL_PACKAGES
    })
    @Deprecated
    public boolean isAppSourceCertificateTrusted(@NonNull X509Certificate certificate)
            throws CertificateEncodingException {
        return false;
    }
}
