Add Encryption Key Db, table and Dao.
Bug: 278054722
Test: atest AdServicesServiceCoreUnitTests
Change-Id: Id549f31da0665b30e281e8b34d21cdfc40e55f72
diff --git a/adservices/service-core/java/com/android/adservices/data/adselection/AdSelectionEncryptionDatabase.java b/adservices/service-core/java/com/android/adservices/data/adselection/AdSelectionEncryptionDatabase.java
new file mode 100644
index 0000000..c7ec9b9
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/data/adselection/AdSelectionEncryptionDatabase.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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 com.android.adservices.data.adselection;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.TypeConverters;
+
+import com.android.adservices.data.common.FledgeRoomConverters;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Objects;
+
+/** Room based database for ad selection encryption keys. */
+@Database(
+ entities = {DBEncryptionKey.class},
+ version = AdSelectionEncryptionDatabase.ENCRYPTION_DATABASE_VERSION)
+@TypeConverters({FledgeRoomConverters.class})
+public abstract class AdSelectionEncryptionDatabase extends RoomDatabase {
+ public static final int ENCRYPTION_DATABASE_VERSION = 1;
+ public static final String ENCRYPTION_DATABASE_NAME = "adselectionencryption.db";
+
+ private static final Object SINGLETON_LOCK = new Object();
+
+ @GuardedBy("SINGLETON_LOCK")
+ private static AdSelectionEncryptionDatabase sSingleton = null;
+
+ /** Returns an instance of the AdSelectionEncryptionDatabase given a context. */
+ public static AdSelectionEncryptionDatabase getInstance(@NonNull Context context) {
+ Objects.requireNonNull(context, "Context must be present.");
+ synchronized (SINGLETON_LOCK) {
+ if (Objects.isNull(sSingleton)) {
+ sSingleton =
+ Room.databaseBuilder(
+ context,
+ AdSelectionEncryptionDatabase.class,
+ ENCRYPTION_DATABASE_NAME)
+ .fallbackToDestructiveMigration()
+ .build();
+ }
+ return sSingleton;
+ }
+ }
+
+ /** @return a Dao to access entities in EncryptionKey database. */
+ public abstract EncryptionKeyDao encryptionKeyDao();
+}
diff --git a/adservices/service-core/java/com/android/adservices/data/adselection/DBEncryptionKey.java b/adservices/service-core/java/com/android/adservices/data/adselection/DBEncryptionKey.java
new file mode 100644
index 0000000..fcc0e97
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/data/adselection/DBEncryptionKey.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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 com.android.adservices.data.adselection;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Index;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.AutoValue.CopyAnnotations;
+
+import java.time.Instant;
+
+/** Table representing EncryptionKeys. */
+@AutoValue
+@CopyAnnotations
+@Entity(
+ tableName = "encryption_key",
+ indices = {@Index(value = {"encryption_key_type", "expiry_instant"})},
+ primaryKeys = {"encryption_key_type", "key_identifier"})
+public abstract class DBEncryptionKey {
+ /** Type of Key. */
+ @NonNull
+ @CopyAnnotations
+ @EncryptionKeyConstants.EncryptionKeyType
+ @ColumnInfo(name = "encryption_key_type")
+ public abstract int getEncryptionKeyType();
+
+ /** KeyIdentifier used for versioning the keys. */
+ @NonNull
+ @CopyAnnotations
+ @ColumnInfo(name = "key_identifier")
+ public abstract String getKeyIdentifier();
+
+ /**
+ * The actual public key. Encoding and parsing of this key is dependent on the keyType and will
+ * be managed by the Key Client.
+ */
+ @NonNull
+ @CopyAnnotations
+ @ColumnInfo(name = "public_key")
+ public abstract String getPublicKey();
+
+ /** Instant this EncryptionKey entry was created. */
+ @CopyAnnotations
+ @ColumnInfo(name = "creation_instant")
+ public abstract Instant getCreationInstant();
+
+ /**
+ * Expiry TTL for this encryption key in seconds. This is sent by the server and stored on
+ * device for computing expiry Instant. Clients should directly read the expiryInstant unless
+ * they specifically need to know the expiry ttl seconds reported by the server.
+ */
+ @NonNull
+ @CopyAnnotations
+ @ColumnInfo(name = "expiry_ttl_seconds")
+ public abstract Long getExpiryTtlSeconds();
+
+ /**
+ * Expiry Instant for this encryption key computed as
+ * creationInstant.plusSeconds(expiryTtlSeconds). Clients should use this field to read the key
+ * expiry value instead of computing it from creation instant and expiry ttl seconds.
+ */
+ @NonNull
+ @CopyAnnotations
+ @ColumnInfo(name = "expiry_instant")
+ public abstract Instant getExpiryInstant();
+
+ /** Returns an AutoValue builder for a {@link DBEncryptionKey} entity. */
+ @NonNull
+ public static DBEncryptionKey.Builder builder() {
+ return new AutoValue_DBEncryptionKey.Builder();
+ }
+
+ /**
+ * Creates a {@link DBEncryptionKey} object using the builder.
+ *
+ * <p>Required for Room SQLite integration.
+ */
+ @NonNull
+ public static DBEncryptionKey create(
+ @EncryptionKeyConstants.EncryptionKeyType int encryptionKeyType,
+ String keyIdentifier,
+ String publicKey,
+ Instant creationInstant,
+ Long expiryTtlSeconds,
+ Instant expiryInstant) {
+
+ return builder()
+ .setEncryptionKeyType(encryptionKeyType)
+ .setKeyIdentifier(keyIdentifier)
+ .setPublicKey(publicKey)
+ .setCreationInstant(creationInstant)
+ .setExpiryInstant(expiryInstant)
+ .setExpiryTtlSeconds(expiryTtlSeconds)
+ .build();
+ }
+
+ /** Builder class for a {@link DBEncryptionKey}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ /** Sets encryption key tupe. */
+ public abstract Builder setEncryptionKeyType(
+ @EncryptionKeyConstants.EncryptionKeyType int encryptionKeyType);
+
+ /** Identifier used to identify the encryptionKey. */
+ public abstract Builder setKeyIdentifier(String keyIdentifier);
+
+ /** Public key of an asymmetric key pair represented by this encryptionKey. */
+ public abstract Builder setPublicKey(String publicKey);
+
+ /** Ttl in seconds for the EncryptionKey. */
+ public abstract Builder setExpiryTtlSeconds(Long expiryTtlSeconds);
+
+ /** Creation instant for the key. */
+ abstract Builder setCreationInstant(Instant creationInstant);
+
+ /** Expiry instant for the key. */
+ abstract Builder setExpiryInstant(Instant expiryInstant);
+
+ abstract Instant getCreationInstant();
+
+ abstract Instant getExpiryInstant();
+
+ abstract Long getExpiryTtlSeconds();
+
+ abstract DBEncryptionKey autoBuild();
+
+ /** Builds the key based on the set values after validating the input. */
+ public final DBEncryptionKey build() {
+ Instant creationInstant = Instant.now();
+ setCreationInstant(creationInstant);
+ setExpiryInstant(creationInstant.plusSeconds(getExpiryTtlSeconds()));
+ return autoBuild();
+ }
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/data/adselection/EncryptionKeyConstants.java b/adservices/service-core/java/com/android/adservices/data/adselection/EncryptionKeyConstants.java
new file mode 100644
index 0000000..29723a7
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/data/adselection/EncryptionKeyConstants.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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 com.android.adservices.data.adselection;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Constants used in the EncryptionKey Datastore. */
+public final class EncryptionKeyConstants {
+ /** IntDef to classify different key types. */
+ @IntDef(
+ value = {
+ EncryptionKeyType.ENCRYPTION_KEY_TYPE_INVALID,
+ EncryptionKeyType.ENCRYPTION_KEY_TYPE_AUCTION,
+ EncryptionKeyType.ENCRYPTION_KEY_TYPE_QUERY,
+ EncryptionKeyType.ENCRYPTION_KEY_TYPE_JOIN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EncryptionKeyType {
+ int ENCRYPTION_KEY_TYPE_INVALID = 0;
+ int ENCRYPTION_KEY_TYPE_AUCTION = 1;
+ int ENCRYPTION_KEY_TYPE_QUERY = 2;
+ int ENCRYPTION_KEY_TYPE_JOIN = 3;
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/data/adselection/EncryptionKeyDao.java b/adservices/service-core/java/com/android/adservices/data/adselection/EncryptionKeyDao.java
new file mode 100644
index 0000000..ecd60d3
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/data/adselection/EncryptionKeyDao.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 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 com.android.adservices.data.adselection;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import java.time.Instant;
+import java.util.List;
+
+/** Dao to manage access to entities in the EncryptionKey table. */
+@Dao
+public abstract class EncryptionKeyDao {
+ /**
+ * Returns the EncryptionKey of given key type with the latest expiry instant.
+ *
+ * @param encryptionKeyType Type of Key to query
+ * @return Returns EncryptionKey with latest expiry instant.
+ */
+ @Query(
+ "SELECT * FROM encryption_key "
+ + "WHERE encryption_key_type = :encryptionKeyType "
+ + "ORDER BY expiry_instant DESC "
+ + "LIMIT 1")
+ public abstract DBEncryptionKey getLatestExpiryKeyOfType(
+ @EncryptionKeyConstants.EncryptionKeyType int encryptionKeyType);
+
+ /**
+ * Returns the EncryptionKey of given key type with the expiry instant higher than given instant
+ * and has the latest expiry instant .
+ */
+ @Query(
+ "SELECT * FROM encryption_key "
+ + "WHERE encryption_key_type = :encryptionKeyType "
+ + "AND expiry_instant >= :now "
+ + "ORDER BY expiry_instant DESC "
+ + "LIMIT 1")
+ public abstract DBEncryptionKey getLatestExpiryActiveKeyOfType(
+ @EncryptionKeyConstants.EncryptionKeyType int encryptionKeyType, Instant now);
+
+ /**
+ * Fetches N number of non-expired EncryptionKey of given key type.
+ *
+ * @param encryptionKeyType Type of EncryptionKey to Query
+ * @param now expiry Instant should be greater than this given instant.
+ * @param count Number of keys to return.
+ * @return
+ */
+ @Query(
+ "SELECT * FROM encryption_key "
+ + "WHERE encryption_key_type = :encryptionKeyType "
+ + "AND expiry_instant >= :now "
+ + "ORDER BY expiry_instant DESC "
+ + "LIMIT :count ")
+ public abstract List<DBEncryptionKey> getLatestExpiryNActiveKeysOfType(
+ @EncryptionKeyConstants.EncryptionKeyType int encryptionKeyType,
+ Instant now,
+ int count);
+
+ /**
+ * Fetches expired keys of given key type. A key is considered expired with its expiryInstant is
+ * lower than the given instant.
+ *
+ * @param type Type of EncryptionKey to Query.
+ * @param now Upper bound instant for expiry determination.
+ * @return Returns expired keys of given key type.
+ */
+ @Query(
+ "SELECT * "
+ + " FROM encryption_key "
+ + "WHERE expiry_instant < :now AND "
+ + "encryption_key_type = :type")
+ public abstract List<DBEncryptionKey> getExpiredKeysForType(
+ @EncryptionKeyConstants.EncryptionKeyType int type, Instant now);
+
+ /**
+ * Returns expired keys in the table.
+ *
+ * @param now A keys is considered expired if key's expiryInstant is lower than this given
+ * instant.
+ * @return Returns expired keys keyed by key type.
+ */
+ @Query("SELECT * FROM encryption_key " + "WHERE expiry_instant < :now ")
+ public abstract List<DBEncryptionKey> getExpiredKeys(Instant now);
+
+ /** Deletes expired keys of the given encryption key type. */
+ @Query("DELETE FROM encryption_key WHERE expiry_instant < :now AND encryption_key_type = :type")
+ public abstract int deleteExpiredRowsByType(
+ @EncryptionKeyConstants.EncryptionKeyType int type, Instant now);
+
+ /** Delete all keys from the table. */
+ @Query("DELETE FROM encryption_key")
+ public abstract int deleteAllEncryptionKeys();
+
+ /** Insert into the table all the given EnryptionKeys. */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract void insertAllKeys(DBEncryptionKey... keys);
+}
diff --git a/adservices/service-core/schemas/com.android.adservices.data.adselection.AdSelectionEncryptionDatabase/1.json b/adservices/service-core/schemas/com.android.adservices.data.adselection.AdSelectionEncryptionDatabase/1.json
new file mode 100644
index 0000000..3f8c6bb
--- /dev/null
+++ b/adservices/service-core/schemas/com.android.adservices.data.adselection.AdSelectionEncryptionDatabase/1.json
@@ -0,0 +1,76 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "c01fb00f9478984f9974c146f3255d17",
+ "entities": [
+ {
+ "tableName": "encryption_key",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`encryption_key_type` INTEGER NOT NULL, `key_identifier` TEXT NOT NULL, `public_key` TEXT NOT NULL, `creation_instant` INTEGER, `expiry_ttl_seconds` INTEGER NOT NULL, `expiry_instant` INTEGER NOT NULL, PRIMARY KEY(`encryption_key_type`, `key_identifier`))",
+ "fields": [
+ {
+ "fieldPath": "encryptionKeyType",
+ "columnName": "encryption_key_type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyIdentifier",
+ "columnName": "key_identifier",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "public_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationInstant",
+ "columnName": "creation_instant",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expiryTtlSeconds",
+ "columnName": "expiry_ttl_seconds",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expiryInstant",
+ "columnName": "expiry_instant",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "encryption_key_type",
+ "key_identifier"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_encryption_key_encryption_key_type_expiry_instant",
+ "unique": false,
+ "columnNames": [
+ "encryption_key_type",
+ "expiry_instant"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_encryption_key_encryption_key_type_expiry_instant` ON `${TABLE_NAME}` (`encryption_key_type`, `expiry_instant`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c01fb00f9478984f9974c146f3255d17')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java
index bea289d..bac58de 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java
@@ -26,6 +26,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.adservices.data.adselection.AdSelectionDatabase;
+import com.android.adservices.data.adselection.AdSelectionEncryptionDatabase;
import com.android.adservices.data.adselection.SharedStorageDatabase;
import com.android.adservices.data.customaudience.CustomAudienceDatabase;
@@ -51,6 +52,7 @@
ImmutableList.of(
CustomAudienceDatabase.class,
AdSelectionDatabase.class,
+ AdSelectionEncryptionDatabase.class,
SharedStorageDatabase.class);
private static final List<DatabaseWithVersion> BYPASS_DATABASE_VERSIONS_NEW_FIELD_ONLY =
ImmutableList.of(new DatabaseWithVersion(CustomAudienceDatabase.class, 2));
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/data/adselection/DBEncryptionKeyTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/data/adselection/DBEncryptionKeyTest.java
new file mode 100644
index 0000000..fc4d22b
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/data/adselection/DBEncryptionKeyTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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 com.android.adservices.data.adselection;
+
+import static com.android.adservices.data.adselection.EncryptionKeyConstants.EncryptionKeyType.ENCRYPTION_KEY_TYPE_AUCTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+import java.time.temporal.ChronoUnit;
+
+public class DBEncryptionKeyTest {
+
+ private static final String KEY_ID_1 = "key_id_1";
+ private static final String PUBLIC_KEY_1 = "public_key_1";
+ private static final Long EXPIRY_TTL_SECONDS_1 = 1209600L;
+
+ @Test
+ public void testBuildValidEncryptionKey_success() {
+ DBEncryptionKey dBEncryptionKey =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier(KEY_ID_1)
+ .setPublicKey(PUBLIC_KEY_1)
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS_1)
+ .build();
+
+ assertThat(dBEncryptionKey.getKeyIdentifier()).isEqualTo(KEY_ID_1);
+ assertThat(dBEncryptionKey.getPublicKey()).isEqualTo(PUBLIC_KEY_1);
+ assertThat(dBEncryptionKey.getEncryptionKeyType()).isEqualTo(ENCRYPTION_KEY_TYPE_AUCTION);
+ assertThat(dBEncryptionKey.getExpiryTtlSeconds()).isEqualTo(EXPIRY_TTL_SECONDS_1);
+ }
+
+ @Test
+ public void testBuildEncryptionKey_unsetKeyIdentifier_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ DBEncryptionKey.builder()
+ .setPublicKey(PUBLIC_KEY_1)
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS_1)
+ .build());
+ }
+
+ @Test
+ public void testBuildEncryptionKey_unsetPublicKey_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ DBEncryptionKey.builder()
+ .setKeyIdentifier(KEY_ID_1)
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS_1)
+ .build());
+ }
+
+ @Test
+ public void testBuildEncryptionKey_unsetEncryptionKeyType_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ DBEncryptionKey.builder()
+ .setKeyIdentifier(KEY_ID_1)
+ .setPublicKey(PUBLIC_KEY_1)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS_1)
+ .build());
+ }
+
+ @Test
+ public void testBuildEncryptionKey_unsetExpiryTtlSeconds_throws() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ DBEncryptionKey.builder()
+ .setKeyIdentifier(KEY_ID_1)
+ .setPublicKey(PUBLIC_KEY_1)
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .build());
+ }
+
+ @Test
+ public void testBuildEncryptionKey_expiryInstantSetCorrectly() {
+ DBEncryptionKey encryptionKey =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier(KEY_ID_1)
+ .setPublicKey(PUBLIC_KEY_1)
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .setExpiryTtlSeconds(5L)
+ .build();
+ assertThat(
+ encryptionKey
+ .getCreationInstant()
+ .plusSeconds(5L)
+ .truncatedTo(ChronoUnit.MILLIS))
+ .isEqualTo(encryptionKey.getExpiryInstant().truncatedTo(ChronoUnit.MILLIS));
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/data/adselection/EncryptionKeyDaoTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/data/adselection/EncryptionKeyDaoTest.java
new file mode 100644
index 0000000..6e23b07
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/data/adselection/EncryptionKeyDaoTest.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2023 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 com.android.adservices.data.adselection;
+
+import static com.android.adservices.data.adselection.EncryptionKeyConstants.EncryptionKeyType.ENCRYPTION_KEY_TYPE_AUCTION;
+import static com.android.adservices.data.adselection.EncryptionKeyConstants.EncryptionKeyType.ENCRYPTION_KEY_TYPE_JOIN;
+import static com.android.adservices.data.adselection.EncryptionKeyConstants.EncryptionKeyType.ENCRYPTION_KEY_TYPE_QUERY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.room.Room;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class EncryptionKeyDaoTest {
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final Long EXPIRY_TTL_SECONDS = 1209600L;
+ private static final DBEncryptionKey ENCRYPTION_KEY_AUCTION =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier("key_id_1")
+ .setPublicKey("public_key_1")
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS)
+ .build();
+
+ private static final DBEncryptionKey ENCRYPTION_KEY_JOIN =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier("key_id_2")
+ .setPublicKey("public_key_2")
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_JOIN)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS)
+ .build();
+
+ private static final DBEncryptionKey ENCRYPTION_KEY_QUERY =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier("key_id_3")
+ .setPublicKey("public_key_3")
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_QUERY)
+ .setExpiryTtlSeconds(EXPIRY_TTL_SECONDS)
+ .build();
+ private static final DBEncryptionKey ENCRYPTION_KEY_AUCTION_TTL_5SECS =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier("key_id_4")
+ .setPublicKey("public_key_4")
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .setExpiryTtlSeconds(5L)
+ .build();
+
+ private static final DBEncryptionKey ENCRYPTION_KEY_JOIN_TTL_5SECS =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier("key_id_5")
+ .setPublicKey("public_key_5")
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_JOIN)
+ .setExpiryTtlSeconds(5L)
+ .build();
+
+ private static final DBEncryptionKey ENCRYPTION_KEY_QUERY_TTL_5SECS =
+ DBEncryptionKey.builder()
+ .setKeyIdentifier("key_id_6")
+ .setPublicKey("public_key_6")
+ .setEncryptionKeyType(ENCRYPTION_KEY_TYPE_QUERY)
+ .setExpiryTtlSeconds(5L)
+ .build();
+
+ private EncryptionKeyDao mEncryptionKeyDao;
+
+ @Before
+ public void setup() {
+ mEncryptionKeyDao =
+ Room.inMemoryDatabaseBuilder(CONTEXT, AdSelectionEncryptionDatabase.class)
+ .build()
+ .encryptionKeyDao();
+ }
+
+ @Test
+ public void test_doesKeyOfTypeExists_returnsTrueWhenKeyExists() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION, ENCRYPTION_KEY_JOIN, ENCRYPTION_KEY_QUERY);
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNotNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_JOIN))
+ .isNotNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_QUERY))
+ .isNotNull();
+ }
+
+ @Test
+ public void test_doesKeyOfTypeExists_returnsFalseWhenKeyAbsent() {
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_JOIN)).isNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_QUERY)).isNull();
+ }
+
+ @Test
+ public void test_getHighestExpiryKeyOfType_returnsEmptyMapWhenKeyAbsent() {
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_JOIN)).isNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_QUERY)).isNull();
+ }
+
+ @Test
+ public void test_getHighestExpiryKeyOfType_returnsFreshestKey() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION,
+ ENCRYPTION_KEY_JOIN,
+ ENCRYPTION_KEY_QUERY,
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS,
+ ENCRYPTION_KEY_JOIN_TTL_5SECS,
+ ENCRYPTION_KEY_QUERY_TTL_5SECS);
+
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION)
+ .getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_AUCTION.getKeyIdentifier());
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_QUERY)
+ .getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_QUERY.getKeyIdentifier());
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_JOIN)
+ .getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_JOIN.getKeyIdentifier());
+ }
+
+ @Test
+ public void test_getHighestExpiryActiveKeyOfType_returnsEmptyMapWhenKeyAbsent() {
+ assertThat(
+ mEncryptionKeyDao.getLatestExpiryActiveKeyOfType(
+ ENCRYPTION_KEY_TYPE_AUCTION, Instant.now()))
+ .isNull();
+ assertThat(
+ mEncryptionKeyDao.getLatestExpiryActiveKeyOfType(
+ ENCRYPTION_KEY_TYPE_JOIN, Instant.now()))
+ .isNull();
+ assertThat(
+ mEncryptionKeyDao.getLatestExpiryActiveKeyOfType(
+ ENCRYPTION_KEY_TYPE_QUERY, Instant.now()))
+ .isNull();
+ }
+
+ @Test
+ public void test_getHighestExpiryActiveKeyOfType_returnsFreshestKey() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION,
+ ENCRYPTION_KEY_JOIN,
+ ENCRYPTION_KEY_QUERY,
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS,
+ ENCRYPTION_KEY_JOIN_TTL_5SECS,
+ ENCRYPTION_KEY_QUERY_TTL_5SECS);
+
+ Instant currentInstant = Instant.now().minusSeconds(3600L);
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryActiveKeyOfType(
+ ENCRYPTION_KEY_TYPE_AUCTION, currentInstant)
+ .getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_AUCTION.getKeyIdentifier());
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryActiveKeyOfType(
+ ENCRYPTION_KEY_TYPE_QUERY, currentInstant)
+ .getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_QUERY.getKeyIdentifier());
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryActiveKeyOfType(
+ ENCRYPTION_KEY_TYPE_JOIN, currentInstant)
+ .getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_JOIN.getKeyIdentifier());
+ }
+
+ @Test
+ public void test_getHighestExpiryNActiveKeyOfType_returnsEmptyMapWhenKeyAbsent() {
+ assertThat(
+ mEncryptionKeyDao.getLatestExpiryNActiveKeysOfType(
+ ENCRYPTION_KEY_TYPE_AUCTION, Instant.now(), 2))
+ .isEmpty();
+ assertThat(
+ mEncryptionKeyDao.getLatestExpiryNActiveKeysOfType(
+ ENCRYPTION_KEY_TYPE_JOIN, Instant.now(), 2))
+ .isEmpty();
+ assertThat(
+ mEncryptionKeyDao.getLatestExpiryNActiveKeysOfType(
+ ENCRYPTION_KEY_TYPE_QUERY, Instant.now(), 2))
+ .isEmpty();
+ }
+
+ @Test
+ public void test_getHighestExpiryNActiveKeyOfType_returnsNFreshestKey() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION,
+ ENCRYPTION_KEY_JOIN,
+ ENCRYPTION_KEY_QUERY,
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS,
+ ENCRYPTION_KEY_JOIN_TTL_5SECS,
+ ENCRYPTION_KEY_QUERY_TTL_5SECS);
+
+ Instant currentInstant = Instant.now().minusSeconds(3600L);
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryNActiveKeysOfType(
+ ENCRYPTION_KEY_TYPE_AUCTION, currentInstant, 2)
+ .stream()
+ .map(k -> k.getKeyIdentifier())
+ .collect(Collectors.toSet()))
+ .containsExactlyElementsIn(
+ ImmutableList.of(
+ ENCRYPTION_KEY_AUCTION.getKeyIdentifier(),
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS.getKeyIdentifier()));
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryNActiveKeysOfType(
+ ENCRYPTION_KEY_TYPE_QUERY, currentInstant, 2)
+ .stream()
+ .map(k -> k.getKeyIdentifier())
+ .collect(Collectors.toSet()))
+ .containsExactlyElementsIn(
+ ImmutableList.of(
+ ENCRYPTION_KEY_QUERY.getKeyIdentifier(),
+ ENCRYPTION_KEY_QUERY_TTL_5SECS.getKeyIdentifier()));
+ assertThat(
+ mEncryptionKeyDao
+ .getLatestExpiryNActiveKeysOfType(
+ ENCRYPTION_KEY_TYPE_JOIN, currentInstant, 2)
+ .stream()
+ .map(k -> k.getKeyIdentifier())
+ .collect(Collectors.toSet()))
+ .containsExactlyElementsIn(
+ ImmutableList.of(
+ ENCRYPTION_KEY_JOIN.getKeyIdentifier(),
+ ENCRYPTION_KEY_JOIN_TTL_5SECS.getKeyIdentifier()));
+ }
+
+ @Test
+ public void test_getExpiredKeysForType_noExpiredKeys_returnsEmpty() {
+ assertThat(
+ mEncryptionKeyDao.getExpiredKeysForType(
+ ENCRYPTION_KEY_TYPE_AUCTION, Instant.now()))
+ .isEmpty();
+ assertThat(mEncryptionKeyDao.getExpiredKeysForType(ENCRYPTION_KEY_TYPE_JOIN, Instant.now()))
+ .isEmpty();
+ assertThat(
+ mEncryptionKeyDao.getExpiredKeysForType(
+ ENCRYPTION_KEY_TYPE_QUERY, Instant.now()))
+ .isEmpty();
+ }
+
+ @Test
+ public void test_getExpiredKeysForType_returnsExpiredKeys_success() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION,
+ ENCRYPTION_KEY_JOIN,
+ ENCRYPTION_KEY_QUERY,
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS,
+ ENCRYPTION_KEY_JOIN_TTL_5SECS,
+ ENCRYPTION_KEY_QUERY_TTL_5SECS);
+
+ Instant currentInstant = Instant.now().plusSeconds(5L);
+ List<DBEncryptionKey> expiredAuctionKeys =
+ mEncryptionKeyDao.getExpiredKeysForType(
+ ENCRYPTION_KEY_TYPE_AUCTION, currentInstant);
+ assertThat(expiredAuctionKeys.size()).isEqualTo(1);
+ assertThat(expiredAuctionKeys.stream().findFirst().get().getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_AUCTION_TTL_5SECS.getKeyIdentifier());
+
+ List<DBEncryptionKey> expiredJoinKeys =
+ mEncryptionKeyDao.getExpiredKeysForType(ENCRYPTION_KEY_TYPE_JOIN, currentInstant);
+ assertThat(expiredJoinKeys.size()).isEqualTo(1);
+ assertThat(expiredJoinKeys.stream().findFirst().get().getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_JOIN_TTL_5SECS.getKeyIdentifier());
+
+ List<DBEncryptionKey> expiredQueryKeys =
+ mEncryptionKeyDao.getExpiredKeysForType(ENCRYPTION_KEY_TYPE_QUERY, currentInstant);
+ assertThat(expiredQueryKeys.size()).isEqualTo(1);
+ assertThat(expiredQueryKeys.stream().findFirst().get().getKeyIdentifier())
+ .isEqualTo(ENCRYPTION_KEY_QUERY_TTL_5SECS.getKeyIdentifier());
+ }
+
+ @Test
+ public void test_getExpiredKeys_noExpiredKeys_returnsEmpty() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION, ENCRYPTION_KEY_JOIN, ENCRYPTION_KEY_QUERY);
+ assertThat(mEncryptionKeyDao.getExpiredKeys(Instant.now())).isEmpty();
+ }
+
+ @Test
+ public void test_getExpiredKeys_returnsExpiredKeys() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION,
+ ENCRYPTION_KEY_JOIN,
+ ENCRYPTION_KEY_QUERY,
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS,
+ ENCRYPTION_KEY_JOIN_TTL_5SECS,
+ ENCRYPTION_KEY_QUERY_TTL_5SECS);
+
+ Instant currentInstant = Instant.now().plusSeconds(5L);
+ List<DBEncryptionKey> expiredKeys = mEncryptionKeyDao.getExpiredKeys(currentInstant);
+
+ assertThat(expiredKeys.size()).isEqualTo(3);
+ assertThat(expiredKeys.stream().map(k -> k.getKeyIdentifier()).collect(Collectors.toSet()))
+ .containsExactly(
+ ENCRYPTION_KEY_AUCTION_TTL_5SECS.getKeyIdentifier(),
+ ENCRYPTION_KEY_JOIN_TTL_5SECS.getKeyIdentifier(),
+ ENCRYPTION_KEY_QUERY_TTL_5SECS.getKeyIdentifier());
+ }
+
+ @Test
+ public void test_deleteExpiredKeys_noExpiredKeys_returnsZero() {
+ mEncryptionKeyDao.insertAllKeys(ENCRYPTION_KEY_AUCTION);
+ assertThat(
+ mEncryptionKeyDao.deleteExpiredRowsByType(
+ ENCRYPTION_KEY_TYPE_AUCTION, Instant.now()))
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void test_deleteExpiredKeys_deletesKeysSuccessfully() {
+ mEncryptionKeyDao.insertAllKeys(ENCRYPTION_KEY_JOIN, ENCRYPTION_KEY_AUCTION_TTL_5SECS);
+
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNotNull();
+
+ mEncryptionKeyDao.deleteExpiredRowsByType(
+ ENCRYPTION_KEY_TYPE_AUCTION, Instant.now().plusSeconds(10L));
+
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNull();
+ }
+
+ @Test
+ public void test_insertAllKeys_validKeys_success() {
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNull();
+ mEncryptionKeyDao.insertAllKeys(ENCRYPTION_KEY_AUCTION);
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNotNull();
+ }
+
+ @Test
+ public void test_deleteAllEncryptionKeys_success() {
+ mEncryptionKeyDao.insertAllKeys(
+ ENCRYPTION_KEY_AUCTION, ENCRYPTION_KEY_JOIN, ENCRYPTION_KEY_QUERY);
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNotNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_JOIN))
+ .isNotNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_QUERY))
+ .isNotNull();
+
+ mEncryptionKeyDao.deleteAllEncryptionKeys();
+
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_AUCTION))
+ .isNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_JOIN)).isNull();
+ assertThat(mEncryptionKeyDao.getLatestExpiryKeyOfType(ENCRYPTION_KEY_TYPE_QUERY)).isNull();
+ }
+}