Add "KeysetHandle#equalsKeyset".

This allows to compare two keysets, which is useful when one wants to have a guarantee that the two keysets are equal (for example, if one gets one from a KMS and the other from disk, but wants no change).

#tinkPublicApiChange

PiperOrigin-RevId: 546812109
diff --git a/java_src/src/main/java/com/google/crypto/tink/Key.java b/java_src/src/main/java/com/google/crypto/tink/Key.java
index 9dffabf..92311a0 100644
--- a/java_src/src/main/java/com/google/crypto/tink/Key.java
+++ b/java_src/src/main/java/com/google/crypto/tink/Key.java
@@ -58,10 +58,15 @@
   public abstract Integer getIdRequirementOrNull();
 
   /**
-   * Returns true if the key is equal to the passed in key.
+   * Returns true if the key is guaranteed to be equal to {@code other}.
    *
    * <p>Implementations are required to do this in constant time.
    *
+   * <p>Note: this is allowed to return false even if two keys are guaranteed to represent the same
+   * function, but are represented differently. For example, a key is allowed to internally store
+   * the number of zero-bytes used as padding when a large number is represented as a byte array,
+   * and use this in the comparison.
+   *
    * <p>Note: Tink {@code Key} objects should typically not override {@code hashCode} (because it
    * could risk leaking key material). Hence, they typically also should not override {@code
    * equals}.
diff --git a/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java b/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java
index 68db882..64ff5bd 100644
--- a/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java
+++ b/java_src/src/main/java/com/google/crypto/tink/KeysetHandle.java
@@ -472,6 +472,22 @@
     public boolean isPrimary() {
       return isPrimary;
     }
+
+    private boolean equalsEntry(Entry other) {
+      if (other.isPrimary != isPrimary) {
+        return false;
+      }
+      if (!other.keyStatus.equals(keyStatus)) {
+        return false;
+      }
+      if (other.id != id) {
+        return false;
+      }
+      if (!other.key.equalsKey(key)) {
+        return false;
+      }
+      return true;
+    }
   }
 
   private static KeyStatus parseStatus(KeyStatusType in) throws GeneralSecurityException {
@@ -1194,4 +1210,39 @@
       return null;
     }
   }
+
+  /**
+   * Returns true if this keyset is equal to {@code other}, ignoring monitoring annotations.
+   *
+   * <p>Note: this may return false even if the keysets represent the same set of functions. For
+   * example, this can happen if the keys store zero-byte padding of a {@link java.math.BigInteger},
+   * which are irrelevant to the function computed. Currently, keysets can also be invalid in which
+   * case this will return false.
+   */
+  boolean equalsKeyset(KeysetHandle other) {
+    if (size() != other.size()) {
+      return false;
+    }
+    boolean primaryFound = false;
+    for (int i = 0; i < size(); ++i) {
+      Entry thisEntry = entries.get(i);
+      Entry otherEntry = other.entries.get(i);
+      if (thisEntry == null) {
+        // Can only happen for invalid keyset
+        return false;
+      }
+      if (otherEntry == null) {
+        // Can only happen for invalid keyset
+        return false;
+      }
+      if (!thisEntry.equalsEntry(otherEntry)) {
+        return false;
+      }
+      primaryFound |= thisEntry.isPrimary;
+    }
+    if (!primaryFound) {
+      return false;
+    }
+    return true;
+  }
 }
diff --git a/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel b/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel
index 1a8c084..adba38e 100644
--- a/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel
+++ b/java_src/src/test/java/com/google/crypto/tink/BUILD.bazel
@@ -323,6 +323,8 @@
         "//src/main/java/com/google/crypto/tink:tink_proto_keyset_format",
         "//src/main/java/com/google/crypto/tink/aead:aead_config",
         "//src/main/java/com/google/crypto/tink/aead:aes_eax_key_manager",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_key",
+        "//src/main/java/com/google/crypto/tink/aead:x_cha_cha20_poly1305_parameters",
         "//src/main/java/com/google/crypto/tink/internal:internal_configuration",
         "//src/main/java/com/google/crypto/tink/internal:key_parser",
         "//src/main/java/com/google/crypto/tink/internal:key_status_type_proto_converter",
diff --git a/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java b/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
index 6e66849..5a508cb 100644
--- a/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/KeysetHandleTest.java
@@ -18,12 +18,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 
 import com.google.common.truth.Expect;
 import com.google.crypto.tink.aead.AeadConfig;
 import com.google.crypto.tink.aead.AesEaxKeyManager;
+import com.google.crypto.tink.aead.XChaCha20Poly1305Key;
+import com.google.crypto.tink.aead.XChaCha20Poly1305Parameters;
 import com.google.crypto.tink.internal.InternalConfiguration;
 import com.google.crypto.tink.internal.KeyParser;
 import com.google.crypto.tink.internal.KeyStatusTypeProtoConverter;
@@ -1705,4 +1709,251 @@
 
     assertThat(keysetHandleMac.computeMac(plaintext)).isEqualTo(registryMac.computeMac(plaintext));
   }
+
+  @Test
+  public void keysetEquality_singleKeyEquals_returnsTrue() throws Exception {
+    SecretBytes bytes = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes))
+                    .withFixedId(101)
+                    .makePrimary())
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes))
+                    .withFixedId(101)
+                    .makePrimary())
+            .build();
+
+    assertTrue(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_singleKeyDifferentKeys_returnsFalse() throws Exception {
+    SecretBytes bytes = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(
+                        XChaCha20Poly1305Key.create(
+                            XChaCha20Poly1305Parameters.Variant.TINK, bytes, 101))
+                    .withFixedId(101)
+                    .makePrimary())
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(
+                        XChaCha20Poly1305Key.create(
+                            XChaCha20Poly1305Parameters.Variant.CRUNCHY, bytes, 101))
+                    .withFixedId(101)
+                    .makePrimary())
+            .build();
+
+    assertFalse(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_singleKeyDifferentId_returnsFalse() throws Exception {
+    SecretBytes bytes = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes))
+                    .withFixedId(102)
+                    .makePrimary())
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes))
+                    .withFixedId(103)
+                    .makePrimary())
+            .build();
+
+    assertFalse(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_twoKeysEquals_returnsTrue() throws Exception {
+    SecretBytes bytes1 = SecretBytes.randomBytes(32);
+    SecretBytes bytes2 = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .build();
+
+    assertTrue(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_twoKeysDifferentPrimaries_returnsFalse() throws Exception {
+    SecretBytes bytes1 = SecretBytes.randomBytes(32);
+    SecretBytes bytes2 = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1)).withFixedId(101))
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2))
+                    .withFixedId(102)
+                    .makePrimary())
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .build();
+
+    assertFalse(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_twoKeysDifferentOrder_returnsFalse() throws Exception {
+    SecretBytes bytes1 = SecretBytes.randomBytes(32);
+    SecretBytes bytes2 = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .build();
+
+    assertFalse(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_twoKeysDifferentStatuses_returnsFalse() throws Exception {
+    SecretBytes bytes1 = SecretBytes.randomBytes(32);
+    SecretBytes bytes2 = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2))
+                    .withFixedId(102)
+                    .setStatus(KeyStatus.DISABLED))
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .build();
+
+    assertFalse(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_twoKeysDifferentSizes_returnsFalse() throws Exception {
+    SecretBytes bytes1 = SecretBytes.randomBytes(32);
+    SecretBytes bytes2 = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .addEntry(KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes2)).withFixedId(102))
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes1))
+                    .withFixedId(101)
+                    .makePrimary())
+            .build();
+
+    assertFalse(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
+
+  @Test
+  public void keysetEquality_unparseableStatus_returnsFalse() throws Exception {
+    Keyset.Key key1 =
+        TestUtil.createKey(
+            TestUtil.createHmacKeyData("01234567890123456".getBytes(UTF_8), 16),
+            42,
+            KeyStatusType.UNKNOWN_STATUS,
+            OutputPrefixType.TINK);
+    KeysetHandle badKeyset = KeysetHandle.fromKeyset(TestUtil.createKeyset(key1));
+    assertFalse(badKeyset.equalsKeyset(badKeyset));
+  }
+
+  @Test
+  public void keysetEquality_noPrimary_returnsFalse() throws Exception {
+    Keyset.Key key1 =
+        TestUtil.createKey(
+            TestUtil.createHmacKeyData("01234567890123456".getBytes(UTF_8), 16),
+            42,
+            KeyStatusType.ENABLED,
+            OutputPrefixType.TINK);
+    Keyset keyset = TestUtil.createKeyset(key1);
+    KeysetHandle badKeyset =
+        KeysetHandle.fromKeyset(Keyset.newBuilder(keyset).setPrimaryKeyId(77).build());
+    assertFalse(badKeyset.equalsKeyset(badKeyset));
+  }
+
+  @Test
+  public void keysetEquality_monitoringAnnotationIgnored_returnsTrue() throws Exception {
+    SecretBytes bytes = SecretBytes.randomBytes(32);
+
+    KeysetHandle keysetHandle1 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes))
+                    .withFixedId(101)
+                    .makePrimary())
+            .setMonitoringAnnotations(MonitoringAnnotations.newBuilder().add("k1", "v1").build())
+            .build();
+    KeysetHandle keysetHandle2 =
+        KeysetHandle.newBuilder()
+            .addEntry(
+                KeysetHandle.importKey(XChaCha20Poly1305Key.create(bytes))
+                    .withFixedId(101)
+                    .makePrimary())
+            .setMonitoringAnnotations(MonitoringAnnotations.newBuilder().add("k2", "v2").build())
+            .build();
+
+    assertTrue(keysetHandle1.equalsKeyset(keysetHandle2));
+  }
 }