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();
+    }
+}