Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e7694cc5 authored by Kevin Chyn's avatar Kevin Chyn
Browse files

1/n: Allow LockSettingsService to return Gatekeeper Password

For certain scenarios, it's ideal if a single prompt for the user's
credential could generate multiple Gatekeeper HATs, each containing
a distinct challenge. To do so, we expose the gatekeeper password
to the caller, which can then be sent to LockSettingsService to
mint a Gatekeeper HAT with a challenge specified by the caller.

Functionally, this is split into two pieces:
1) ILockSettings#verifyCredential* has a new flags parameter,
   which if contains VERIFY_FLAG_RETURN_GK_PW, returns the gatekeeper
   password
2) ILockSettings introduces a new method, verifyGatekeeperPassword,
   which takes the Gatekeeper Password and challenge, from which
   Gatekeeper creates a HardwareAuthToken. This is different than
   the rest of spBasedDoverifyCredential and __only__ requests
   Gatekeeper to create the HardwareAuthToken. It does not proceed
   to do other things such as unlocking keystore keys, unlocking
   managed profiles, etc.

Slightly cleaned up VerifyCredentialResponse: moved to builder pattern,
cleaned up serialization/deserialization

Returning a VerifyCredentialResponse object (instead of a byte[])
also makes it easier to debug failure cases (e.g. credential was
verified but HAT was null, vs originally we have no idea). Similarly,
this allows us to remove RequestThrottledException, which can help
make it easier to reason about code flow (less unexpected nullness)

Test: Clients with VERIFY_FLAG_RETURN_GK_PW have correct "accept,
      reject, timeout" behavior
Test: Current biometric enrollment works and not affected
Test: PIN/Pattern/Password verifyGatekeeperPassword works
      (see ag/12222644)

Bug: 161765592
Change-Id: I6e2a7ea234aac1a278b35cdaff62b1c7e3e9f205
parent 637d43d6
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -47,8 +47,9 @@ interface ILockSettings {
    void resetKeyStore(int userId);
    VerifyCredentialResponse checkCredential(in LockscreenCredential credential, int userId,
            in ICheckCredentialProgressCallback progressCallback);
    VerifyCredentialResponse verifyCredential(in LockscreenCredential credential, long challenge, int userId);
    VerifyCredentialResponse verifyTiedProfileChallenge(in LockscreenCredential credential, long challenge, int userId);
    VerifyCredentialResponse verifyCredential(in LockscreenCredential credential, long challenge, int userId, int flags);
    VerifyCredentialResponse verifyTiedProfileChallenge(in LockscreenCredential credential, long challenge, int userId, int flags);
    VerifyCredentialResponse verifyGatekeeperPassword(in byte[] gatekeeperPassword, long challenge, int userId);
    boolean checkVoldPassword(int userId);
    int getCredentialType(int userId);
    byte[] getHashFactor(in LockscreenCredential currentCredential, int userId);
+24 −22
Original line number Diff line number Diff line
package com.android.internal.widget;

import android.annotation.NonNull;
import android.os.AsyncTask;

import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
@@ -41,11 +42,11 @@ public final class LockPatternChecker {
        /**
         * Invoked when a security verification is finished.
         *
         * @param attestation The attestation that the challenge was verified, or null.
         * @param response The response, optionally containing Gatekeeper HAT or Gatekeeper Password
         * @param throttleTimeoutMs The amount of time in ms to wait before reattempting
         * the call. Only non-0 if attestation is null.
         * the call. Only non-0 if the response is {@link VerifyCredentialResponse#RESPONSE_RETRY}.
         */
        void onVerified(byte[] attestation, int throttleTimeoutMs);
        void onVerified(@NonNull VerifyCredentialResponse response, int throttleTimeoutMs);
    }

    /**
@@ -55,31 +56,31 @@ public final class LockPatternChecker {
     * @param credential The credential to check.
     * @param challenge The challenge to verify against the credential.
     * @param userId The user to check against the credential.
     * @param flags See {@link LockPatternUtils.VerifyFlag}
     * @param callback The callback to be invoked with the verification result.
     */
    public static AsyncTask<?, ?, ?> verifyCredential(final LockPatternUtils utils,
            final LockscreenCredential credential,
            final long challenge,
            final int userId,
            final @LockPatternUtils.VerifyFlag int flags,
            final OnVerifyCallback callback) {
        // Create a copy of the credential since checking credential is asynchrounous.
        final LockscreenCredential credentialCopy = credential.duplicate();
        AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
            private int mThrottleTimeout;

        AsyncTask<Void, Void, VerifyCredentialResponse> task =
                new AsyncTask<Void, Void, VerifyCredentialResponse>() {
            @Override
            protected byte[] doInBackground(Void... args) {
            protected VerifyCredentialResponse doInBackground(Void... args) {
                try {
                    return utils.verifyCredential(credentialCopy, challenge, userId);
                    return utils.verifyCredential(credentialCopy, challenge, userId, flags);
                } catch (RequestThrottledException ex) {
                    mThrottleTimeout = ex.getTimeoutMs();
                    return null;
                    return VerifyCredentialResponse.fromTimeout(ex.getTimeoutMs());
                }
            }

            @Override
            protected void onPostExecute(byte[] result) {
                callback.onVerified(result, mThrottleTimeout);
            protected void onPostExecute(@NonNull VerifyCredentialResponse result) {
                callback.onVerified(result, result.getTimeout());
                credentialCopy.zeroize();
            }

@@ -143,31 +144,32 @@ public final class LockPatternChecker {
     * @param credential The credential to check.
     * @param challenge The challenge to verify against the credential.
     * @param userId The user to check against the credential.
     * @param flags See {@link LockPatternUtils.VerifyFlag}
     * @param callback The callback to be invoked with the verification result.
     */
    public static AsyncTask<?, ?, ?> verifyTiedProfileChallenge(final LockPatternUtils utils,
            final LockscreenCredential credential,
            final long challenge,
            final int userId,
            final @LockPatternUtils.VerifyFlag int flags,
            final OnVerifyCallback callback) {
        // Create a copy of the credential since checking credential is asynchrounous.
        // Create a copy of the credential since checking credential is asynchronous.
        final LockscreenCredential credentialCopy = credential.duplicate();
        AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
            private int mThrottleTimeout;

        AsyncTask<Void, Void, VerifyCredentialResponse> task =
                new AsyncTask<Void, Void, VerifyCredentialResponse>() {
            @Override
            protected byte[] doInBackground(Void... args) {
            protected VerifyCredentialResponse doInBackground(Void... args) {
                try {
                    return utils.verifyTiedProfileChallenge(credentialCopy, challenge, userId);
                    return utils.verifyTiedProfileChallenge(credentialCopy, challenge, userId,
                            flags);
                } catch (RequestThrottledException ex) {
                    mThrottleTimeout = ex.getTimeoutMs();
                    return null;
                    return VerifyCredentialResponse.fromTimeout(ex.getTimeoutMs());
                }
            }

            @Override
            protected void onPostExecute(byte[] result) {
                callback.onVerified(result, mThrottleTimeout);
            protected void onPostExecute(@NonNull VerifyCredentialResponse response) {
                callback.onVerified(response, response.getTimeout());
                credentialCopy.zeroize();
            }

+68 −17
Original line number Diff line number Diff line
@@ -129,6 +129,18 @@ public class LockPatternUtils {
    })
    public @interface CredentialType {}

    /**
     * Flag provided to {@link #verifyCredential(LockscreenCredential, long, int, int)} . If set,
     * the method will return the Gatekeeper Password in the {@link VerifyCredentialResponse}.
     */
    public static final int VERIFY_FLAG_RETURN_GK_PW = 1 << 0;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, value = {
            VERIFY_FLAG_RETURN_GK_PW
    })
    public @interface VerifyFlag {}

    /**
     * Special user id for triggering the FRP verification flow.
     */
@@ -376,27 +388,56 @@ public class LockPatternUtils {
     * @param credential The credential to check.
     * @param challenge The challenge to verify against the credential
     * @param userId The user whose credential is being verified
     * @return the attestation that the challenge was verified, or null
     * @param flags See {@link VerifyFlag}
     * @throws RequestThrottledException if credential verification is being throttled due to
     *         to many incorrect attempts.
     * @throws IllegalStateException if called on the main thread.
     *
     * TODO: This probably doesn't need to throw RequestThrottledException anymore, since the
     * method now returns a VerifyCredentialResponse object, which contains the timeout
     */
    public byte[] verifyCredential(@NonNull LockscreenCredential credential, long challenge,
            int userId) throws RequestThrottledException {
    @NonNull
    public VerifyCredentialResponse verifyCredential(@NonNull LockscreenCredential credential,
            long challenge, int userId, @VerifyFlag int flags) throws RequestThrottledException {
        throwIfCalledOnMainThread();
        try {
            VerifyCredentialResponse response = getLockSettings().verifyCredential(
                    credential, challenge, userId);
            final VerifyCredentialResponse response = getLockSettings().verifyCredential(
                    credential, challenge, userId, flags);
            if (response == null) {
                return VerifyCredentialResponse.ERROR;
            }

            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
                return response.getPayload();
                return response;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
                throw new RequestThrottledException(response.getTimeout());
            } else {
                return null;
                return VerifyCredentialResponse.ERROR;
            }
        } catch (RemoteException re) {
            Log.e(TAG, "failed to verify credential", re);
            return null;
            return VerifyCredentialResponse.ERROR;
        }
    }

    /**
     * With the Gatekeeper Password returned via {@link #verifyCredential(LockscreenCredential,
     * long, int, boolean)}, request Gatekeeper to create a HardwareAuthToken wrapping the
     * given challenge.
     */
    @NonNull
    public VerifyCredentialResponse verifyGatekeeperPassword(@NonNull byte[] gatekeeperPassword,
            long challenge, int userId) {
        try {
            final VerifyCredentialResponse response = getLockSettings().verifyGatekeeperPassword(
                    gatekeeperPassword, challenge, userId);
            if (response == null) {
                return VerifyCredentialResponse.ERROR;
            }
            return response;
        } catch (RemoteException e) {
            Log.e(TAG, "failed to verify gatekeeper password", e);
            return VerifyCredentialResponse.ERROR;
        }
    }

@@ -418,8 +459,9 @@ public class LockPatternUtils {
        try {
            VerifyCredentialResponse response = getLockSettings().checkCredential(
                    credential, userId, wrapCallback(progressCallback));

            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
            if (response == null) {
                return false;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
                return true;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
                throw new RequestThrottledException(response.getTimeout());
@@ -442,27 +484,36 @@ public class LockPatternUtils {
     * @param challenge The challenge to verify against the credential
     * @return the attestation that the challenge was verified, or null
     * @param userId The managed profile user id
     * @param flags See {@link VerifyFlag}
     * @throws RequestThrottledException if credential verification is being throttled due to
     *         to many incorrect attempts.
     * @throws IllegalStateException if called on the main thread.
     *
     * TODO: This probably doesn't need to throw RequestThrottledException anymore, since the
     * method now returns a VerifyCredentialResponse object, which contains the timeout
     */
    public byte[] verifyTiedProfileChallenge(@NonNull LockscreenCredential credential,
            long challenge, int userId) throws RequestThrottledException {
    @NonNull
    public VerifyCredentialResponse verifyTiedProfileChallenge(
            @NonNull LockscreenCredential credential,
            long challenge, int userId, @VerifyFlag int flags) throws RequestThrottledException {
        throwIfCalledOnMainThread();
        try {
            VerifyCredentialResponse response =
                    getLockSettings().verifyTiedProfileChallenge(credential, challenge, userId);
            final VerifyCredentialResponse response = getLockSettings()
                    .verifyTiedProfileChallenge(credential, challenge, userId, flags);
            if (response == null) {
                return VerifyCredentialResponse.ERROR;
            }

            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
                return response.getPayload();
                return response;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
                throw new RequestThrottledException(response.getTimeout());
            } else {
                return null;
                return VerifyCredentialResponse.ERROR;
            }
        } catch (RemoteException re) {
            Log.e(TAG, "failed to verify tied profile credential", re);
            return null;
            return VerifyCredentialResponse.ERROR;
        }
    }

+1 −1
Original line number Diff line number Diff line
@@ -48,7 +48,7 @@ import java.util.Objects;
 *     // Process the credential in some way
 * }
 * </pre>
 * With this construct, we can garantee that there will be no copies of the password left in
 * With this construct, we can guarantee that there will be no copies of the password left in
 * memory when the credential goes out of scope. This should help mitigate certain class of
 * attacks where the attcker gains read-only access to full device memory (cold boot attack,
 * unsecured software/hardware memory dumping interfaces such as JTAG).
+105 −61
Original line number Diff line number Diff line
@@ -16,11 +16,16 @@

package com.android.internal.widget;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.gatekeeper.GateKeeperResponse;
import android.util.Slog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Response object for a ILockSettings credential verification request.
 * @hide
@@ -30,78 +35,114 @@ public final class VerifyCredentialResponse implements Parcelable {
    public static final int RESPONSE_ERROR = -1;
    public static final int RESPONSE_OK = 0;
    public static final int RESPONSE_RETRY = 1;

    public static final VerifyCredentialResponse OK = new VerifyCredentialResponse();
    public static final VerifyCredentialResponse ERROR
            = new VerifyCredentialResponse(RESPONSE_ERROR, 0, null);
    @IntDef({RESPONSE_ERROR,
            RESPONSE_OK,
            RESPONSE_RETRY})
    @Retention(RetentionPolicy.SOURCE)
    @interface ResponseCode {}

    public static final VerifyCredentialResponse OK = new VerifyCredentialResponse.Builder()
            .build();
    public static final VerifyCredentialResponse ERROR = fromError();
    private static final String TAG = "VerifyCredentialResponse";

    private int mResponseCode;
    private byte[] mPayload;
    private int mTimeout;
    private final @ResponseCode int mResponseCode;
    private final int mTimeout;
    @Nullable private final byte[] mGatekeeperHAT;
    @Nullable private final byte[] mGatekeeperPw;

    public static final Parcelable.Creator<VerifyCredentialResponse> CREATOR
            = new Parcelable.Creator<VerifyCredentialResponse>() {
        @Override
        public VerifyCredentialResponse createFromParcel(Parcel source) {
            int responseCode = source.readInt();
            VerifyCredentialResponse response = new VerifyCredentialResponse(responseCode, 0, null);
            if (responseCode == RESPONSE_RETRY) {
                response.setTimeout(source.readInt());
            } else if (responseCode == RESPONSE_OK) {
                int size = source.readInt();
                if (size > 0) {
                    byte[] payload = new byte[size];
                    source.readByteArray(payload);
                    response.setPayload(payload);
                }
            }
            return response;
            final @ResponseCode int responseCode = source.readInt();
            final int timeout = source.readInt();
            final byte[] gatekeeperHAT = source.createByteArray();
            final byte[] gatekeeperPassword = source.createByteArray();

            return new VerifyCredentialResponse(responseCode, timeout, gatekeeperHAT,
                    gatekeeperPassword);
        }

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

    };

    public VerifyCredentialResponse() {
        mResponseCode = RESPONSE_OK;
        mPayload = null;
    public static class Builder {
        @Nullable private byte[] mGatekeeperHAT;
        @Nullable private byte[] mGatekeeperPassword;

        /**
         * @param gatekeeperHAT Gatekeeper HardwareAuthToken, minted upon successful authentication.
         */
        public Builder setGatekeeperHAT(byte[] gatekeeperHAT) {
            mGatekeeperHAT = gatekeeperHAT;
            return this;
        }

        public Builder setGatekeeperPassword(byte[] gatekeeperPassword) {
            mGatekeeperPassword = gatekeeperPassword;
            return this;
        }

    public VerifyCredentialResponse(byte[] payload) {
        mPayload = payload;
        mResponseCode = RESPONSE_OK;
        /**
         * Builds a VerifyCredentialResponse with {@link #RESPONSE_OK} and any other parameters
         * that were preveiously set.
         * @return
         */
        public VerifyCredentialResponse build() {
            return new VerifyCredentialResponse(RESPONSE_OK,
                    0 /* timeout */,
                    mGatekeeperHAT,
                    mGatekeeperPassword);
        }
    }

    public VerifyCredentialResponse(int timeout) {
        mTimeout = timeout;
        mResponseCode = RESPONSE_RETRY;
        mPayload = null;
    /**
     * Since timeouts are always an error, provide a way to create the VerifyCredentialResponse
     * object directly. None of the other fields (Gatekeeper HAT, Gatekeeper Password, etc)
     * are valid in this case. Similarly, the response code will always be
     * {@link #RESPONSE_RETRY}.
     */
    public static VerifyCredentialResponse fromTimeout(int timeout) {
        return new VerifyCredentialResponse(RESPONSE_RETRY,
                timeout,
                null /* gatekeeperHAT */,
                null /* gatekeeperPassword */);
    }

    private VerifyCredentialResponse(int responseCode, int timeout, byte[] payload) {
    /**
     * Since error (incorrect password) should never result in any of the other fields from
     * being populated, provide a default method to return a VerifyCredentialResponse.
     */
    public static VerifyCredentialResponse fromError() {
        return new VerifyCredentialResponse(RESPONSE_ERROR,
                0 /* timeout */,
                null /* gatekeeperHAT */,
                null /* gatekeeperPassword */);
    }

    private VerifyCredentialResponse(@ResponseCode int responseCode, int timeout,
            @Nullable byte[] gatekeeperHAT, @Nullable byte[] gatekeeperPassword) {
        mResponseCode = responseCode;
        mTimeout = timeout;
        mPayload = payload;
        mGatekeeperHAT = gatekeeperHAT;
        mGatekeeperPw = gatekeeperPassword;
    }

    public VerifyCredentialResponse stripPayload() {
        return new VerifyCredentialResponse(mResponseCode, mTimeout,
                null /* gatekeeperHAT */, null /* gatekeeperPassword */);
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mResponseCode);
        if (mResponseCode == RESPONSE_RETRY) {
        dest.writeInt(mTimeout);
        } else if (mResponseCode == RESPONSE_OK) {
            if (mPayload != null) {
                dest.writeInt(mPayload.length);
                dest.writeByteArray(mPayload);
            } else {
                dest.writeInt(0);
            }
        }
        dest.writeByteArray(mGatekeeperHAT);
        dest.writeByteArray(mGatekeeperPw);
    }

    @Override
@@ -109,48 +150,51 @@ public final class VerifyCredentialResponse implements Parcelable {
        return 0;
    }

    public byte[] getPayload() {
        return mPayload;
    @Nullable
    public byte[] getGatekeeperHAT() {
        return mGatekeeperHAT;
    }

    @Nullable
    public byte[] getGatekeeperPw() {
        return mGatekeeperPw;
    }

    public int getTimeout() {
        return mTimeout;
    }

    public int getResponseCode() {
    public @ResponseCode int getResponseCode() {
        return mResponseCode;
    }

    private void setTimeout(int timeout) {
        mTimeout = timeout;
    }

    private void setPayload(byte[] payload) {
        mPayload = payload;
    public boolean isMatched() {
        return mResponseCode == RESPONSE_OK;
    }

    public VerifyCredentialResponse stripPayload() {
        return new VerifyCredentialResponse(mResponseCode, mTimeout, new byte[0]);
    @Override
    public String toString() {
        return "Response: " + mResponseCode
                + ", GK HAT: " + (mGatekeeperHAT != null)
                + ", GK PW: " + (mGatekeeperPw != null);
    }

    public static VerifyCredentialResponse fromGateKeeperResponse(
            GateKeeperResponse gateKeeperResponse) {
        VerifyCredentialResponse response;
        int responseCode = gateKeeperResponse.getResponseCode();
        if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
            response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
            return fromTimeout(gateKeeperResponse.getTimeout());
        } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
            byte[] token = gateKeeperResponse.getPayload();
            if (token == null) {
                // something's wrong if there's no payload with a challenge
                Slog.e(TAG, "verifyChallenge response had no associated payload");
                response = VerifyCredentialResponse.ERROR;
                return fromError();
            } else {
                response = new VerifyCredentialResponse(token);
                return new VerifyCredentialResponse.Builder().setGatekeeperHAT(token).build();
            }
        } else {
            response = VerifyCredentialResponse.ERROR;
            return fromError();
        }
        return response;
    }
}
Loading