1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.federatedcompute.services.data;
18 
19 import static com.android.federatedcompute.services.data.ODPAuthorizationTokenContract.ODP_AUTHORIZATION_TOKEN_TABLE;
20 
21 import android.annotation.NonNull;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteException;
27 
28 import com.android.federatedcompute.internal.util.LogUtil;
29 import com.android.federatedcompute.services.data.ODPAuthorizationTokenContract.ODPAuthorizationTokenColumns;
30 import com.android.odp.module.common.Clock;
31 import com.android.odp.module.common.MonotonicClock;
32 
33 import com.google.common.annotations.VisibleForTesting;
34 
35 public class ODPAuthorizationTokenDao {
36     private static final String TAG = ODPAuthorizationTokenDao.class.getSimpleName();
37 
38     private final FederatedComputeDbHelper mDbHelper;
39 
40     private final Clock mClock;
41 
42     private static volatile ODPAuthorizationTokenDao sSingletonInstance;
43 
ODPAuthorizationTokenDao(FederatedComputeDbHelper dbHelper, Clock clock)44     private ODPAuthorizationTokenDao(FederatedComputeDbHelper dbHelper, Clock clock) {
45         mDbHelper = dbHelper;
46         mClock = clock;
47     }
48 
49     /**
50      * @return an instance of ODPAuthorizationTokenDao given a context
51      */
52     @NonNull
getInstance(Context context)53     public static ODPAuthorizationTokenDao getInstance(Context context) {
54         if (sSingletonInstance == null) {
55             synchronized (ODPAuthorizationTokenDao.class) {
56                 if (sSingletonInstance == null) {
57                     sSingletonInstance = new ODPAuthorizationTokenDao(
58                             FederatedComputeDbHelper.getInstance(context),
59                             MonotonicClock.getInstance()
60                     );
61                 }
62             }
63         }
64         return sSingletonInstance;
65     }
66 
67     /** Return a test instance with in-memory database. It is for test only. */
68     @VisibleForTesting
getInstanceForTest(Context context)69     public static ODPAuthorizationTokenDao getInstanceForTest(Context context) {
70         if (sSingletonInstance == null) {
71             synchronized (ODPAuthorizationTokenDao.class) {
72                 if (sSingletonInstance == null) {
73                     sSingletonInstance = new ODPAuthorizationTokenDao(
74                             FederatedComputeDbHelper.getInstanceForTest(context),
75                             MonotonicClock.getInstance()
76                     );
77                 }
78             }
79         }
80         return sSingletonInstance;
81     }
82 
83     /** Insert a token to the odp authorization token table. */
insertAuthorizationToken(ODPAuthorizationToken authorizationToken)84     public boolean insertAuthorizationToken(ODPAuthorizationToken authorizationToken) {
85         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
86         if (db == null) {
87             throw new SQLiteException(TAG + ": Failed to open database.");
88         }
89 
90         ContentValues values = new ContentValues();
91         values.put(ODPAuthorizationTokenColumns.OWNER_IDENTIFIER,
92                 authorizationToken.getOwnerIdentifier());
93         values.put(ODPAuthorizationTokenColumns.AUTHORIZATION_TOKEN,
94                 authorizationToken.getAuthorizationToken());
95         values.put(ODPAuthorizationTokenColumns.CREATION_TIME,
96                 authorizationToken.getCreationTime());
97         values.put(ODPAuthorizationTokenColumns.EXPIRY_TIME,
98                 authorizationToken.getExpiryTime());
99 
100 
101         long jobId =
102                 db.insertWithOnConflict(
103                         ODP_AUTHORIZATION_TOKEN_TABLE, "", values, SQLiteDatabase.CONFLICT_REPLACE);
104         return jobId != -1;
105     }
106 
107     /** Get an ODP adopter's unexpired authorization token.
108      * @return an unexpired authorization token. */
getUnexpiredAuthorizationToken(String ownerIdentifier)109     public ODPAuthorizationToken getUnexpiredAuthorizationToken(String ownerIdentifier) {
110         String selection = ODPAuthorizationTokenColumns.EXPIRY_TIME + " > ? " + "AND "
111                 + ODPAuthorizationTokenColumns.OWNER_IDENTIFIER + " = ?";
112         String[] selectionArgs = {String.valueOf(mClock.currentTimeMillis()), ownerIdentifier};
113         String orderBy = ODPAuthorizationTokenColumns.EXPIRY_TIME + " DESC";
114         return readTokenFromDatabase(selection, selectionArgs, orderBy);
115     }
116 
117     /** Delete an ODP adopter's authorization token.
118      * @return the number of rows deleted. */
deleteAuthorizationToken(String ownerIdentifier)119     public int deleteAuthorizationToken(String ownerIdentifier) {
120         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
121         if (db == null) {
122             throw new SQLiteException(TAG + ": Failed to open database.");
123         }
124         String whereClause = ODPAuthorizationTokenColumns.OWNER_IDENTIFIER + " = ?";
125         String[] whereArgs = {ownerIdentifier};
126         int deletedRows = db.delete(ODP_AUTHORIZATION_TOKEN_TABLE, whereClause, whereArgs);
127         LogUtil.d(TAG, "Deleted %d expired tokens for %s from database", deletedRows,
128                 ownerIdentifier);
129         return deletedRows;
130     }
131 
132 
133     /** Batch delete all expired authorization tokens.
134      * @return the number of rows deleted. */
deleteExpiredAuthorizationTokens()135     public int deleteExpiredAuthorizationTokens() {
136         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
137         if (db == null) {
138             throw new SQLiteException(TAG + ": Failed to open database.");
139         }
140         String whereClause = ODPAuthorizationTokenColumns.EXPIRY_TIME + " < ?";
141         String[] whereArgs = { String.valueOf(mClock.currentTimeMillis()) };
142         int deletedRows = db.delete(ODP_AUTHORIZATION_TOKEN_TABLE, whereClause, whereArgs);
143         LogUtil.d(TAG, "Deleted %d expired tokens", deletedRows);
144         return deletedRows;
145     }
146 
147 
148 
readTokenFromDatabase( String selection, String[] selectionArgs, String orderBy)149     private ODPAuthorizationToken readTokenFromDatabase(
150             String selection, String[] selectionArgs, String orderBy) {
151         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
152         if (db == null) {
153             throw new SQLiteException(TAG + ": Failed to open database.");
154         }
155 
156         String[] selectColumns = {
157                 ODPAuthorizationTokenColumns.OWNER_IDENTIFIER,
158                 ODPAuthorizationTokenColumns.AUTHORIZATION_TOKEN,
159                 ODPAuthorizationTokenColumns.CREATION_TIME,
160                 ODPAuthorizationTokenColumns.EXPIRY_TIME,
161         };
162 
163         Cursor cursor = null;
164         ODPAuthorizationToken authToken = null;
165         try {
166             cursor =
167                     db.query(
168                             ODP_AUTHORIZATION_TOKEN_TABLE,
169                             selectColumns,
170                             selection,
171                             selectionArgs,
172                             /* groupBy= */ null,
173                             /* having= */ null,
174                             /* orderBy= */ orderBy,
175                             /* limit= */ String.valueOf(1));
176             while (cursor.moveToNext()) {
177                 ODPAuthorizationToken.Builder encryptionKeyBuilder =
178                         new ODPAuthorizationToken.Builder(
179                                 cursor.getString(
180                                         cursor.getColumnIndexOrThrow(
181                                                 ODPAuthorizationTokenColumns.OWNER_IDENTIFIER)),
182                                 cursor.getString(
183                                         cursor.getColumnIndexOrThrow(
184                                                 ODPAuthorizationTokenColumns.AUTHORIZATION_TOKEN)),
185                                 cursor.getLong(
186                                         cursor.getColumnIndexOrThrow(
187                                                 ODPAuthorizationTokenColumns.CREATION_TIME)),
188                                 cursor.getLong(
189                                         cursor.getColumnIndexOrThrow(
190                                                 ODPAuthorizationTokenColumns.EXPIRY_TIME))
191                         );
192                 authToken = encryptionKeyBuilder.build();
193             }
194         } finally {
195             if (cursor != null) {
196                 cursor.close();
197             }
198         }
199         return authToken;
200     }
201 }
202