[Privacy] Compare app signing certificates before restoring
backed up permissions to apps

Resubmitting with fix, see http://b/247728455#comment4 for details on
the fix and http://b/184847040#comment54 for testing rationale.

Earlier Reverted Changes:
If6f0df898:[automerge] Revert "[Privacy] Compare app signing ...
I9fe0701c1:Revert "[Privacy] Compare app signing certificates...
I4f031a63c:Revert "[Privacy] Compare app signing certificates...
Iec42d2fbb:[automerge] Revert "[Privacy] Compare app signing ...

Bug: 184847040
Test: atest CtsSecurityTestCases:PermissionBackupCertificateCheckTest
Merged-In: I9c2c261a5b766759200be0e26d6cdaafc4ce846a
Change-Id: I9c2c261a5b766759200be0e26d6cdaafc4ce846a
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java b/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java
index 0fc6448..9082b69 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/service/BackupHelper.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES;
 import static android.util.Xml.newSerializer;
 
 import static com.android.permissioncontroller.Constants.DELAYED_RESTORE_PERMISSIONS_FILE;
@@ -31,13 +32,17 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.os.Build;
 import android.os.UserHandle;
 import android.permission.PermissionManager;
 import android.permission.PermissionManager.SplitPermissionInfo;
 import android.util.ArraySet;
+import android.util.Base64;
 import android.util.Log;
 import android.util.Xml;
 
@@ -49,6 +54,7 @@
 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
 import com.android.permissioncontroller.permission.model.AppPermissions;
 import com.android.permissioncontroller.permission.model.Permission;
+import com.android.permissioncontroller.permission.utils.CollectionUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -57,8 +63,13 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Helper for creating and restoring permission backups.
@@ -74,6 +85,11 @@
     private static final String TAG_GRANT = "grant";
     private static final String ATTR_PACKAGE_NAME = "pkg";
 
+    private static final String TAG_SIGNING_INFO = "sign";
+    private static final String TAG_CURRENT_CERTIFICATE = "curr-cert";
+    private static final String TAG_PAST_CERTIFICATE = "past-cert";
+    private static final String ATTR_CERTIFICATE_DIGEST = "digest";
+
     private static final String TAG_PERMISSION = "perm";
     private static final String ATTR_PERMISSION_NAME = "name";
     private static final String ATTR_IS_GRANTED = "g";
@@ -228,12 +244,16 @@
                 PackageInfo pkgInfo;
                 try {
                     pkgInfo = mContext.getPackageManager().getPackageInfo(pkgState.mPackageName,
-                            GET_PERMISSIONS);
+                            GET_PERMISSIONS | GET_SIGNING_CERTIFICATES);
                 } catch (PackageManager.NameNotFoundException ignored) {
                     packagesToRestoreLater.add(pkgState);
                     continue;
                 }
 
+                if (!checkCertificateDigestsMatch(pkgInfo, pkgState)) {
+                    continue;
+                }
+
                 pkgState.restore(mContext, pkgInfo);
             }
         }
@@ -244,6 +264,56 @@
     }
 
     /**
+     * Returns whether the backed up package and the package being restored have compatible signing
+     * certificate digests.
+     *
+     * <p> Permissions should only be restored if the backed up package has the same signing
+     * certificate(s) or an ancestor (in the case of certification rotation).
+     *
+     * <p>If no certificates are found stored for the backed up package, we return true anyway as
+     * certificate storage does not exist before {@link Build.VERSION_CODES.TIRAMISU}.
+     */
+    private boolean checkCertificateDigestsMatch(
+            @NonNull PackageInfo packageToRestoreInfo,
+            @NonNull BackupPackageState backupPackageState) {
+        // No signing information was stored for the backed up app.
+        if (backupPackageState.mBackupSigningInfoState == null) {
+            return true;
+        }
+
+        // The backed up app was unsigned.
+        if (backupPackageState.mBackupSigningInfoState.mCurrentCertDigests.isEmpty()) {
+            return false;
+        }
+
+        // We don't have signing information for the restored app, but the backed up app was signed.
+        if (packageToRestoreInfo.signingInfo == null) {
+            return false;
+        }
+
+        // The restored app is unsigned.
+        if (packageToRestoreInfo.signingInfo.getApkContentsSigners() == null
+                || packageToRestoreInfo.signingInfo.getApkContentsSigners().length == 0) {
+            return false;
+        }
+
+        // If the restored app is a system app, we allow permissions to be restored without any
+        // certificate checks.
+        // System apps are signed with the device's platform certificate, so on
+        // different phones the same system app can have different certificates.
+        // We perform this check to be consistent with the Backup and Restore feature logic in
+        // frameworks/base/services/core/java/com/android/server/backup/BackupUtils.java
+        if ((packageToRestoreInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+            return true;
+        }
+
+        // Both backed up app and restored app have signing information, so we check that these are
+        // compatible for the purpose of restoring permissions to the restored app.
+        return hasCompatibleSignaturesForRestore(packageToRestoreInfo.signingInfo,
+                backupPackageState.mBackupSigningInfoState);
+    }
+
+    /**
      * Write a xml file for the given packages.
      *
      * @param serializer The file to write to
@@ -308,7 +378,7 @@
      */
     void writeState(@NonNull XmlSerializer serializer) throws IOException {
         List<PackageInfo> pkgs = mContext.getPackageManager().getInstalledPackages(
-                GET_PERMISSIONS);
+                GET_PERMISSIONS | GET_SIGNING_CERTIFICATES);
         ArrayList<BackupPackageState> backupPkgs = new ArrayList<>();
 
         int numPkgs = pkgs.size();
@@ -348,7 +418,8 @@
 
             PackageInfo pkgInfo = null;
             try {
-                pkgInfo = mContext.getPackageManager().getPackageInfo(packageName, GET_PERMISSIONS);
+                pkgInfo = mContext.getPackageManager().getPackageInfo(
+                        packageName, GET_PERMISSIONS | GET_SIGNING_CERTIFICATES);
             } catch (PackageManager.NameNotFoundException e) {
                 Log.e(LOG_TAG, "Could not restore delayed permissions for " + packageName, e);
             }
@@ -358,7 +429,8 @@
                 for (int i = 0; i < numPkgs; i++) {
                     BackupPackageState pkgState = packagesToRestoreLater.get(i);
 
-                    if (pkgState.mPackageName.equals(packageName)) {
+                    if (pkgState.mPackageName.equals(packageName) && checkCertificateDigestsMatch(
+                            pkgInfo, pkgState)) {
                         pkgState.restore(mContext, pkgInfo);
                         packagesToRestoreLater.remove(i);
 
@@ -377,7 +449,8 @@
      * State that needs to be backed up for a permission.
      */
     private static class BackupPermissionState {
-        private final @NonNull String mPermissionName;
+        @NonNull
+        private final String mPermissionName;
         private final boolean mIsGranted;
         private final boolean mIsUserSet;
         private final boolean mIsUserFixed;
@@ -401,7 +474,8 @@
          *
          * @return The state
          */
-        static @NonNull List<BackupPermissionState> parseFromXml(@NonNull XmlPullParser parser,
+        @NonNull
+        static List<BackupPermissionState> parseFromXml(@NonNull XmlPullParser parser,
                 @NonNull Context context, int backupPlatformVersion)
                 throws XmlPullParserException {
             String permName = parser.getAttributeValue(null, ATTR_PERMISSION_NAME);
@@ -463,7 +537,8 @@
          * @return The state to back up or {@code null} if the permission does not need to be
          * backed up.
          */
-        private static @Nullable BackupPermissionState fromPermission(@NonNull Permission perm,
+        @Nullable
+        private static BackupPermissionState fromPermission(@NonNull Permission perm,
                 boolean appSupportsRuntimePermissions) {
             int grantFlags = perm.getFlags();
 
@@ -502,7 +577,8 @@
          * @return The state to back up. Empty list if no permissions in the group need to be backed
          * up
          */
-        static @NonNull ArrayList<BackupPermissionState> fromPermissionGroup(
+        @NonNull
+        static ArrayList<BackupPermissionState> fromPermissionGroup(
                 @NonNull AppPermissionGroup group) {
             ArrayList<BackupPermissionState> permissionsToRestore = new ArrayList<>();
             List<Permission> perms = group.getPermissions();
@@ -594,17 +670,153 @@
         }
     }
 
+    /** Signing certificate information for a backed up package. */
+    private static class BackupSigningInfoState {
+        @NonNull
+        private final Set<byte[]> mCurrentCertDigests;
+        @NonNull
+        private final Set<byte[]> mPastCertDigests;
+
+        private BackupSigningInfoState(@NonNull Set<byte[]> currentCertDigests,
+                @NonNull Set<byte[]> pastCertDigests) {
+            mCurrentCertDigests = currentCertDigests;
+            mPastCertDigests = pastCertDigests;
+        }
+
+        /**
+         * Write this state as XML.
+         *
+         * @param serializer the file to write to
+         */
+        void writeAsXml(@NonNull XmlSerializer serializer) throws IOException {
+            serializer.startTag(null, TAG_SIGNING_INFO);
+
+            for (byte[] digest : mCurrentCertDigests) {
+                serializer.startTag(null, TAG_CURRENT_CERTIFICATE);
+                serializer.attribute(
+                        null, ATTR_CERTIFICATE_DIGEST,
+                        Base64.encodeToString(digest, Base64.NO_WRAP));
+                serializer.endTag(null, TAG_CURRENT_CERTIFICATE);
+            }
+
+            for (byte[] digest : mPastCertDigests) {
+                serializer.startTag(null, TAG_PAST_CERTIFICATE);
+                serializer.attribute(
+                        null, ATTR_CERTIFICATE_DIGEST,
+                        Base64.encodeToString(digest, Base64.NO_WRAP));
+                serializer.endTag(null, TAG_PAST_CERTIFICATE);
+            }
+
+            serializer.endTag(null, TAG_SIGNING_INFO);
+        }
+
+        /**
+         * Parse the signing information state from XML.
+         *
+         * @param parser the data to read
+         *
+         * @return the signing information state
+         */
+        @NonNull
+        static BackupSigningInfoState parseFromXml(@NonNull XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            Set<byte[]> currentCertDigests = new HashSet<>();
+            Set<byte[]> pastCertDigests = new HashSet<>();
+
+            while (true) {
+                switch (parser.next()) {
+                    case START_TAG:
+                        switch (parser.getName()) {
+                            case TAG_CURRENT_CERTIFICATE:
+                                String currentCertDigest =
+                                        parser.getAttributeValue(
+                                                null, ATTR_CERTIFICATE_DIGEST);
+                                if (currentCertDigest == null) {
+                                    throw new XmlPullParserException(
+                                            "Found " + TAG_CURRENT_CERTIFICATE + " without "
+                                                    + ATTR_CERTIFICATE_DIGEST);
+                                }
+                                currentCertDigests.add(
+                                        Base64.decode(currentCertDigest, Base64.NO_WRAP));
+                                skipToEndOfTag(parser);
+                                break;
+                            case TAG_PAST_CERTIFICATE:
+                                String pastCertDigest =
+                                        parser.getAttributeValue(
+                                                null, ATTR_CERTIFICATE_DIGEST);
+                                if (pastCertDigest == null) {
+                                    throw new XmlPullParserException(
+                                            "Found " + TAG_PAST_CERTIFICATE + " without "
+                                                    + ATTR_CERTIFICATE_DIGEST);
+                                }
+                                pastCertDigests.add(
+                                        Base64.decode(pastCertDigest, Base64.NO_WRAP));
+                                skipToEndOfTag(parser);
+                                break;
+                            default:
+                                Log.w(LOG_TAG, "Found unexpected tag " + parser.getName());
+                                skipToEndOfTag(parser);
+                        }
+
+                        break;
+                    case END_TAG:
+                        return new BackupSigningInfoState(
+                                currentCertDigests,
+                                pastCertDigests);
+                    default:
+                        throw new XmlPullParserException("Could not parse signing info");
+                }
+            }
+        }
+
+        /**
+         * Construct the signing information state from a {@link SigningInfo} instance.
+         *
+         * @param signingInfo the {@link SigningInfo} instance
+         *
+         * @return the state
+         */
+        @NonNull
+        static BackupSigningInfoState fromSigningInfo(@NonNull SigningInfo signingInfo) {
+            Set<byte[]> currentCertDigests = new HashSet<>();
+            Set<byte[]> pastCertDigests = new HashSet<>();
+
+            Signature[] apkContentsSigners = signingInfo.getApkContentsSigners();
+            for (int i = 0; i < apkContentsSigners.length; i++) {
+                currentCertDigests.add(
+                        computeSha256DigestBytes(apkContentsSigners[i].toByteArray()));
+            }
+
+            if (signingInfo.hasPastSigningCertificates()) {
+                Signature[] signingCertificateHistory = signingInfo.getSigningCertificateHistory();
+                for (int i = 0; i < signingCertificateHistory.length; i++) {
+                    pastCertDigests.add(
+                            computeSha256DigestBytes(signingCertificateHistory[i].toByteArray()));
+                }
+            }
+
+            return new BackupSigningInfoState(currentCertDigests, pastCertDigests);
+        }
+    }
+
     /**
      * State that needs to be backed up for a package.
      */
     private static class BackupPackageState {
-        final @NonNull String mPackageName;
-        private final @NonNull ArrayList<BackupPermissionState> mPermissionsToRestore;
+        @NonNull
+        final String mPackageName;
+        @NonNull
+        private final ArrayList<BackupPermissionState> mPermissionsToRestore;
+        @Nullable
+        private final BackupSigningInfoState mBackupSigningInfoState;
 
-        private BackupPackageState(@NonNull String packageName,
-                @NonNull ArrayList<BackupPermissionState> permissionsToRestore) {
+        private BackupPackageState(
+                @NonNull String packageName,
+                @NonNull ArrayList<BackupPermissionState> permissionsToRestore,
+                @Nullable BackupSigningInfoState backupSigningInfoState) {
             mPackageName = packageName;
             mPermissionsToRestore = permissionsToRestore;
+            mBackupSigningInfoState = backupSigningInfoState;
         }
 
         /**
@@ -616,7 +828,8 @@
          *
          * @return The state
          */
-        static @NonNull BackupPackageState parseFromXml(@NonNull XmlPullParser parser,
+        @NonNull
+        static BackupPackageState parseFromXml(@NonNull XmlPullParser parser,
                 @NonNull Context context, int backupPlatformVersion)
                 throws IOException, XmlPullParserException {
             String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
@@ -626,6 +839,7 @@
             }
 
             ArrayList<BackupPermissionState> permissionsToRestore = new ArrayList<>();
+            BackupSigningInfoState signingInfo = null;
 
             while (true) {
                 switch (parser.next()) {
@@ -643,6 +857,16 @@
 
                                 skipToEndOfTag(parser);
                                 break;
+                            case TAG_SIGNING_INFO:
+                                try {
+                                    signingInfo = BackupSigningInfoState.parseFromXml(parser);
+                                } catch (XmlPullParserException e) {
+                                    Log.e(LOG_TAG, "Could not parse signing info for "
+                                            + packageName, e);
+                                    skipToEndOfTag(parser);
+                                }
+
+                                break;
                             default:
                                 // ignore tag
                                 Log.w(LOG_TAG, "Found unexpected tag " + parser.getName()
@@ -652,7 +876,10 @@
 
                         break;
                     case END_TAG:
-                        return new BackupPackageState(packageName, permissionsToRestore);
+                        return new BackupPackageState(
+                                packageName,
+                                permissionsToRestore,
+                                signingInfo);
                     case END_DOCUMENT:
                         throw new XmlPullParserException("Could not parse state for "
                                 + packageName);
@@ -669,7 +896,8 @@
          * @return The state to back up or {@code null} if no permission of the package need to be
          * backed up.
          */
-        static @Nullable BackupPackageState fromAppPermissions(@NonNull Context context,
+        @Nullable
+        static BackupPackageState fromAppPermissions(@NonNull Context context,
                 @NonNull PackageInfo pkgInfo) {
             AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, null);
 
@@ -694,7 +922,14 @@
                 return null;
             }
 
-            return new BackupPackageState(pkgInfo.packageName, permissionsToRestore);
+            BackupSigningInfoState signingInfoState = null;
+
+            if (pkgInfo.signingInfo != null) {
+                signingInfoState = BackupSigningInfoState.fromSigningInfo(pkgInfo.signingInfo);
+            }
+
+            return new BackupPackageState(
+                    pkgInfo.packageName, permissionsToRestore, signingInfoState);
         }
 
         /**
@@ -715,6 +950,10 @@
                 mPermissionsToRestore.get(i).writeAsXml(serializer);
             }
 
+            if (mBackupSigningInfoState != null) {
+                mBackupSigningInfoState.writeAsXml(serializer);
+            }
+
             serializer.endTag(null, TAG_GRANT);
         }
 
@@ -760,4 +999,100 @@
             appPerms.persistChanges(true, affectedPermissions);
         }
     }
+
+    /**
+     * Returns whether the signing certificates of the restored app and backed up app are
+     * compatible for the restored app to be granted the backed up app's permissions.
+     *
+     * <p>This returns true when any one of the following is true:
+     *
+     * <ul>
+     *     <li> the backed up app has multiple signing certificates and the restored app
+     *     has identical multiple signing certificates
+     *     <li> the backed up app has a single signing certificate and it is the current
+     *     single signing certificate of the restored app
+     *     <li> the backed up app has a single signing certificate and it is present in the
+     *     signing certificate history of the restored app
+     *     <li> the backed up app has a single signing certificate and signing certificate
+     *     history, and the signing certificate of the restored app is present in that history
+     * </ul>*
+     */
+    private boolean hasCompatibleSignaturesForRestore(@NonNull SigningInfo restoredSigningInfo,
+            @NonNull BackupSigningInfoState backupSigningInfoState) {
+        Set<byte[]> backupCertDigests = backupSigningInfoState.mCurrentCertDigests;
+        Set<byte[]> backupPastCertDigests = backupSigningInfoState.mPastCertDigests;
+        Signature[] restoredSignatures = restoredSigningInfo.getApkContentsSigners();
+
+        // Check that both apps have the same number of signing certificates. This will be a
+        // required check for both the single and multiple certificate cases.
+        if (backupCertDigests.size() != restoredSignatures.length) {
+            return false;
+        }
+
+        Set<byte[]> restoredCertDigests = new HashSet<>();
+        for (Signature signature: restoredSignatures) {
+            restoredCertDigests.add(computeSha256DigestBytes(signature.toByteArray()));
+        }
+
+        // If the backed up app has multiple signing certificates, the restored app should be
+        // signed by that exact set of multiple signing certificates.
+        if (backupCertDigests.size() > 1) {
+            // Check that the restored certificates are a subset of the backed up certificates.
+            if (!CollectionUtils.containsSubset(backupCertDigests, restoredCertDigests)) {
+                return false;
+            }
+            // Check that the backed up certificates are a subset of the restored certificates.
+            if (!CollectionUtils.containsSubset(restoredCertDigests, backupCertDigests)) {
+                return false;
+            }
+            return true;
+        }
+
+        // If both apps have a single signing certificate, we check if they are equal or if one
+        // app's certificate is in the signing certificate history of the other.
+        byte[] backupCertDigest = backupCertDigests.iterator().next();
+        byte[] restoredPastCertDigest = restoredCertDigests.iterator().next();
+
+        // Check if the backed up app and restored app have the same signing certificate.
+        if (Arrays.equals(backupCertDigest, restoredPastCertDigest)) {
+            return true;
+        }
+
+        // Check if the restored app's certificate is in the backed up app's signing certificate
+        // history.
+        if (CollectionUtils.contains(backupPastCertDigests, restoredPastCertDigest)) {
+            return true;
+        }
+
+        // Check if the backed up app's certificate is in the restored app's signing certificate
+        // history.
+        if (restoredSigningInfo.hasPastSigningCertificates()) {
+            // The last element in the pastSigningCertificates array is the current signer;
+            // since that was verified above, just check all the signers in the lineage.
+            for (int i = 0; i < restoredSigningInfo.getSigningCertificateHistory().length - 1;
+                    i++) {
+                restoredPastCertDigest = computeSha256DigestBytes(
+                        restoredSigningInfo.getSigningCertificateHistory()[i].toByteArray());
+                if (Arrays.equals(backupCertDigest, restoredPastCertDigest)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** Computes the SHA256 digest of the provided {@code byte} array. */
+    @Nullable
+    private static byte[] computeSha256DigestBytes(@NonNull byte[] data) {
+        MessageDigest messageDigest;
+        try {
+            messageDigest = MessageDigest.getInstance("SHA256");
+        } catch (NoSuchAlgorithmException e) {
+            return null;
+        }
+
+        messageDigest.update(data);
+
+        return messageDigest.digest();
+    }
 }
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/CollectionUtils.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/CollectionUtils.java
index 4581f4e..6f2a307 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/CollectionUtils.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/CollectionUtils.java
@@ -21,9 +21,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Utility methods for dealing with {@link java.util.Collection}s.
@@ -93,4 +95,43 @@
     public static <T> List<T> singletonOrEmpty(@Nullable T element) {
         return element != null ? Collections.singletonList(element) : Collections.emptyList();
     }
+
+    /**
+     * Returns whether a byte array is contained within a {@link Set} of byte arrays. Equality is
+     * not compared by reference, but by comparing the elements contained in the arrays.
+     *
+     * @param byteArrays a {@link Set} of byte arrays that will be searched
+     * @param otherByteArray byte array to be searched
+     * @return {@code true} if {@code byteArrays} contains a byte array with identical elements as
+     *         {@code otherByteArray}.
+     */
+    public static boolean contains(@NonNull Set<byte[]> byteArrays,
+            @NonNull byte[] otherByteArray) {
+        for (byte[] byteArray : byteArrays) {
+            if (Arrays.equals(byteArray, otherByteArray)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether a {@link Set} of byte arrays is contained within a {@link Set} of byte arrays
+     * as a subset. Equality for arrays is not compared by reference, but by comparing the elements
+     * contained in the arrays.
+     *
+     * @param byteArrays a {@link Set} of byte arrays which will be checked as a superset
+     * @param otherByteArrays a {@link Set} of byte arrays which be checked as a subset
+     * @return {@code true} if {@code byteArrays} contains all the arrays in {@code
+     *     otherByteArrays}.
+     */
+    public static boolean containsSubset(
+            @NonNull Set<byte[]> byteArrays, @NonNull Set<byte[]> otherByteArrays) {
+        for (byte[] byteArray : otherByteArrays) {
+            if (!contains(byteArrays, byteArray)) {
+                return false;
+            }
+        }
+        return true;
+    }
 }