Merge "Map RkpProxyException error codes into get key errors"
diff --git a/core/java/android/security/rkp/IGetKeyCallback.aidl b/core/java/android/security/rkp/IGetKeyCallback.aidl
index 85ceae62..a36d054 100644
--- a/core/java/android/security/rkp/IGetKeyCallback.aidl
+++ b/core/java/android/security/rkp/IGetKeyCallback.aidl
@@ -25,6 +25,36 @@
  * @hide
  */
 oneway interface IGetKeyCallback {
+    enum ErrorCode {
+        /**
+         * An unexpected error occurred and there's no standard way to describe it. See the
+         * corresponding error string for more information.
+         */
+        ERROR_UNKNOWN = 1,
+
+        /**
+         * Device will not receive remotely provisioned keys because it's running vulnerable
+         * code. The device needs to be updated to a fixed build to recover.
+         */
+        ERROR_REQUIRES_SECURITY_PATCH = 2,
+
+        /**
+         * Indicates that the attestation key pool has been exhausted, and the remote key
+         * provisioning server cannot currently be reached. Clients should wait for the
+         * device to have connectivity, then retry.
+         */
+        ERROR_PENDING_INTERNET_CONNECTIVITY = 3,
+
+        /**
+         * Indicates that this device will never be able to provision attestation keys using
+         * the remote provsisioning server. This may be due to multiple causes, such as the
+         * device is not registered with the remote provisioning backend or the device has
+         * been permanently revoked. Clients who receive this error should not attempt to
+         * retry key creation.
+         */
+        ERROR_PERMANENT = 5,
+    }
+
     /**
      * Called in response to {@link IRegistration.getKey}, indicating
      * a remotely-provisioned key is available.
@@ -42,8 +72,9 @@
     /**
      * Called when an error has occurred while trying to get a remotely provisioned key.
      *
-     * @param error A description of what failed, suitable for logging.
+     * @param error allows code to handle certain errors, if desired
+     * @param description human-readable explanation of what failed, suitable for logging.
      */
-    void onError(String error);
+    void onError(ErrorCode error, String description);
 }
 
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
index 0e92709..2d3ede0 100644
--- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
@@ -24,6 +24,7 @@
 import android.security.rkp.IStoreUpgradedKeyCallback;
 import android.security.rkp.service.RegistrationProxy;
 import android.security.rkp.service.RemotelyProvisionedKey;
+import android.security.rkp.service.RkpProxyException;
 import android.util.Log;
 
 import java.util.Set;
@@ -68,13 +69,35 @@
             if (e instanceof OperationCanceledException) {
                 Log.i(TAG, "Operation cancelled for client " + mCallback.hashCode());
                 wrapCallback(mCallback::onCancel);
+            } else if (e instanceof RkpProxyException) {
+                Log.e(TAG, "RKP error fetching key for client " + mCallback.hashCode(), e);
+                RkpProxyException rkpException = (RkpProxyException) e;
+                wrapCallback(() -> mCallback.onError(toGetKeyError(rkpException),
+                        e.getMessage()));
             } else {
                 Log.e(TAG, "Error fetching key for client " + mCallback.hashCode(), e);
-                wrapCallback(() -> mCallback.onError(e.getMessage()));
+                wrapCallback(() -> mCallback.onError(IGetKeyCallback.ErrorCode.ERROR_UNKNOWN,
+                        e.getMessage()));
             }
         }
     }
 
+    private byte toGetKeyError(RkpProxyException exception) {
+        switch (exception.getError()) {
+            case RkpProxyException.ERROR_UNKNOWN:
+                return IGetKeyCallback.ErrorCode.ERROR_UNKNOWN;
+            case RkpProxyException.ERROR_REQUIRES_SECURITY_PATCH:
+                return IGetKeyCallback.ErrorCode.ERROR_REQUIRES_SECURITY_PATCH;
+            case RkpProxyException.ERROR_PENDING_INTERNET_CONNECTIVITY:
+                return IGetKeyCallback.ErrorCode.ERROR_PENDING_INTERNET_CONNECTIVITY;
+            case RkpProxyException.ERROR_PERMANENT:
+                return IGetKeyCallback.ErrorCode.ERROR_PERMANENT;
+            default:
+                Log.e(TAG, "Unexpected error code in RkpProxyException", exception);
+                return IGetKeyCallback.ErrorCode.ERROR_UNKNOWN;
+        }
+    }
+
     RemoteProvisioningRegistration(RegistrationProxy registration, Executor executor) {
         mRegistration = registration;
         mExecutor = executor;
@@ -97,7 +120,8 @@
         } catch (Exception e) {
             Log.e(TAG, "getKeyAsync threw an exception for client " + callback.hashCode(), e);
             mGetKeyOperations.remove(callback);
-            wrapCallback(() -> callback.onError(e.getMessage()));
+            wrapCallback(() -> callback.onError(IGetKeyCallback.ErrorCode.ERROR_UNKNOWN,
+                    e.getMessage()));
         }
     }
 
diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java
index 7b361d3..1dcd0b9 100644
--- a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java
+++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningRegistrationTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.contains;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
@@ -39,6 +40,7 @@
 import android.security.rkp.IStoreUpgradedKeyCallback;
 import android.security.rkp.service.RegistrationProxy;
 import android.security.rkp.service.RemotelyProvisionedKey;
+import android.security.rkp.service.RkpProxyException;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -48,8 +50,9 @@
 import org.mockito.stubbing.Answer;
 import org.mockito.stubbing.VoidAnswer4;
 
-import java.time.Duration;
+import java.lang.reflect.Field;
 import java.util.Arrays;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 /**
@@ -104,7 +107,7 @@
     }
 
     @Test
-    public void getKeyHandlesError() throws Exception {
+    public void getKeyHandlesArbitraryException() throws Exception {
         Exception expectedException = new Exception("oops!");
         doAnswer(
                 answerGetKeyAsync((keyId, cancelSignal, executor, receiver) ->
@@ -112,11 +115,38 @@
                 .when(mRegistrationProxy).getKeyAsync(eq(0), any(), any(), any());
         IGetKeyCallback callback = mock(IGetKeyCallback.class);
         mRegistration.getKey(0, callback);
-        verify(callback).onError(eq(expectedException.getMessage()));
+        verify(callback).onError(eq(IGetKeyCallback.ErrorCode.ERROR_UNKNOWN), eq("oops!"));
         verifyNoMoreInteractions(callback);
     }
 
     @Test
+    public void getKeyMapsRkpErrorsCorrectly() throws Exception {
+        Map<Byte, Integer> expectedConversions = Map.of(
+                IGetKeyCallback.ErrorCode.ERROR_UNKNOWN,
+                RkpProxyException.ERROR_UNKNOWN,
+                IGetKeyCallback.ErrorCode.ERROR_REQUIRES_SECURITY_PATCH,
+                RkpProxyException.ERROR_REQUIRES_SECURITY_PATCH,
+                IGetKeyCallback.ErrorCode.ERROR_PENDING_INTERNET_CONNECTIVITY,
+                RkpProxyException.ERROR_PENDING_INTERNET_CONNECTIVITY,
+                IGetKeyCallback.ErrorCode.ERROR_PERMANENT,
+                RkpProxyException.ERROR_PERMANENT);
+
+        for (Field errorField: IGetKeyCallback.ErrorCode.class.getFields()) {
+            byte error = (Byte) errorField.get(null);
+            Exception expectedException = new RkpProxyException(expectedConversions.get(error),
+                    errorField.getName());
+            doAnswer(
+                    answerGetKeyAsync((keyId, cancelSignal, executor, receiver) ->
+                            executor.execute(() -> receiver.onError(expectedException))))
+                    .when(mRegistrationProxy).getKeyAsync(eq(0), any(), any(), any());
+            IGetKeyCallback callback = mock(IGetKeyCallback.class);
+            mRegistration.getKey(0, callback);
+            verify(callback).onError(eq(error), contains(errorField.getName()));
+            verifyNoMoreInteractions(callback);
+        }
+    }
+
+    @Test
     public void getKeyCancelDuringProxyOperation() throws Exception {
         IGetKeyCallback callback = mock(IGetKeyCallback.class);
         doAnswer(
@@ -179,7 +209,8 @@
 
         IGetKeyCallback callback = mock(IGetKeyCallback.class);
         mRegistration.getKey(0, callback);
-        verify(callback).onError(eq(expectedException.getMessage()));
+        verify(callback).onError(eq(IGetKeyCallback.ErrorCode.ERROR_UNKNOWN),
+                eq(expectedException.getMessage()));
         assertThrows(IllegalArgumentException.class, () -> mRegistration.cancelGetKey(callback));
         verifyNoMoreInteractions(callback);
     }