1 /*
2  * Copyright (C) 2022 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.car.bluetooth;
18 
19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
20 
21 import android.annotation.NonNull;
22 import android.car.builtin.util.Slogf;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.os.UserManager;
26 import android.util.Log;
27 
28 import com.android.car.CarLog;
29 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
30 import com.android.car.internal.util.IndentingPrintWriter;
31 
32 import java.math.BigInteger;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.LinkedHashSet;
36 import java.util.List;
37 import java.util.Objects;
38 
39 import javax.crypto.spec.SecretKeySpec;
40 
41 /**
42  * A collection of 128-bit Account Keys that are received over the Fast Pair protocol.
43  *
44  * The specification requires that we store at least 5 Account Keys, but places no upper bound on
45  * how many we can store. It only mentions that they all must fit in our chosen packet size. To
46  * support this, we have a variable fixed upper bound of the number of stored keys. If you input a
47  * number less than five, it will be adjusted up.
48  *
49  * The specification also requires that we remove the least recently used key if we ever run out of
50  * space. To support this, keys are stored in an LRU cache. Adding a key when storage is full will
51  * automatically remove the least recently used key.
52  *
53  * The specification requires that keys are persisted. To support this, keys are written to the
54  * user's Shared Preferences. There is one preferences for the count of keys, and then a preference
55  * for each key in priority order, where an index preference is mapped to a key value, i.e. the
56  * perference "0" would map to a 128-bit key.
57  *
58  * Keys are loaded from Shared Preferences upon creation of this object.
59  */
60 public class FastPairAccountKeyStorage {
61     private static final String TAG = CarLog.tagFor(FastPairAccountKeyStorage.class);
62     private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
63 
64     private static final String FAST_PAIR_PREFERENCES = "com.android.car.bluetooth";
65     private static final String NUM_ACCOUNT_KEYS = "AccountKeysCount";
66 
67     private final Context mContext;
68 
69     /**
70      * Represents a 128-bit Account Key that can be received through the FastPair process.
71      */
72     public static class AccountKey {
73         private final byte[] mKey;
74 
AccountKey(byte[] key)75         AccountKey(byte[] key) {
76             mKey = key;
77         }
78 
AccountKey(String key)79         AccountKey(String key) {
80             mKey = new BigInteger(key).toByteArray();
81         }
82 
83         /**
84          * Get a byte representation of this Account Key
85          */
toBytes()86         public byte[] toBytes() {
87             return mKey;
88         }
89 
90         /**
91          * Get a SecretKeySpec representation of this Account Key
92          */
getKeySpec()93         public SecretKeySpec getKeySpec() {
94             return new SecretKeySpec(mKey, "AES");
95         }
96 
97         @Override
hashCode()98         public int hashCode() {
99             return Arrays.hashCode(mKey);
100         }
101 
102         @Override
equals(Object obj)103         public boolean equals(Object obj) {
104             if (!(obj instanceof AccountKey)) {
105                 return false;
106             }
107             AccountKey other = (AccountKey) obj;
108             return other != null && Arrays.equals(toBytes(), other.toBytes());
109         }
110 
111         @Override
toString()112         public String toString() {
113             return Arrays.toString(mKey);
114         }
115     }
116 
117     /*
118      * A LinkedHashSet is used as an LRU. Iterating on the LinkedHashSet will produce the items in
119      * the order they were inserted-- first inserted, first iterated on.
120      */
121     private final LinkedHashSet<AccountKey> mKeys;
122     private final Object mKeyLock = new Object();
123     private final int mStorageSize;
124 
FastPairAccountKeyStorage(Context context, int size)125     public FastPairAccountKeyStorage(Context context, int size) {
126         if (size < 5) {
127             throw new IllegalArgumentException("size < 5");
128         }
129         mContext = Objects.requireNonNull(context);
130         mStorageSize = size;
131         mKeys = new LinkedHashSet<AccountKey>(mStorageSize);
132         load(); // A no-op if storage isn't unlocked yet
133     }
134 
135     /**
136      * Get the total number of account keys that can be stored
137      */
capacity()138     public int capacity() {
139         return mStorageSize;
140     }
141 
142     /**
143      * Add an account key
144      */
add(@onNull AccountKey key)145     public boolean add(@NonNull AccountKey key) {
146         if (key == null) return false;
147         Slogf.i("Adding key '%s'", key.toString());
148         synchronized (mKeyLock) {
149             // LinkedHashSet re-adds do not impact the ordering. To force the ordering to update,
150             //  we'll remove the key first if its already in the set, then re-add it.
151             if (mKeys.contains(key)) {
152                 mKeys.remove(key);
153             }
154             mKeys.add(key);
155             trimToSize();
156             commit();
157             return true;
158         }
159     }
160 
161     /**
162      * Remove an account key
163      */
remove(@onNull AccountKey key)164     public boolean remove(@NonNull AccountKey key) {
165         if (key == null) return false;
166         Slogf.i("Removing key '%s'", key.toString());
167         synchronized (mKeyLock) {
168             mKeys.remove(key);
169             commit();
170             return true;
171         }
172     }
173 
174     /**
175      * Get a list of all the available account keys
176      */
getAllAccountKeys()177     public List<AccountKey> getAllAccountKeys() {
178         synchronized (mKeyLock) {
179             return new ArrayList<>(mKeys);
180         }
181     }
182 
183     /**
184      * Clears all account keys from storage
185      */
clear()186     public void clear() {
187         synchronized (mKeyLock) {
188             mKeys.clear();
189             commit();
190         }
191     }
192 
193     /**
194      * Removes the least recently used items until the size of our cache is less than or equal to
195      * our configured maximum size.
196      */
trimToSize()197     private void trimToSize() {
198         while (mKeys.size() > mStorageSize) {
199             AccountKey key = mKeys.iterator().next();
200             mKeys.remove(key);
201             Slogf.d("Evicted key '%s'", key.toString());
202         }
203     }
204 
205     /**
206      * Loads persisted account keys from Shared Preferences
207      *
208      * Account keys are stored in key value pairs of <integer> to <string>, where the integer is the
209      * position in the LRU (higher is more recently used), and the string is a string version of the
210      * bytes. There is also an "AccountKeysCount" preference indicating how many keys are stored.
211      * Keys will have integer keys in the range [0, AccountKeysCount - 1].
212      *
213      * This cannot be called until the user is unlocked.
214      */
load()215     public boolean load() {
216         if (!isUserUnlocked()) {
217             // TODO (243016325): Determine a way to recover from a failed load()
218             Slogf.w(TAG, "Loaded while user was not unlocked. Shared Preferences unavailable");
219             return false;
220         }
221 
222         List<AccountKey> keys = new ArrayList<>();
223         SharedPreferences preferences =
224                 mContext.getSharedPreferences(FAST_PAIR_PREFERENCES, Context.MODE_PRIVATE);
225         int numKeys = preferences.getInt(NUM_ACCOUNT_KEYS, 0);
226 
227         for (int i = 0; i < numKeys; i++) {
228             String key = preferences.getString(Integer.toString(i), null);
229             if (key != null) {
230                 keys.add(new AccountKey(key));
231             }
232         }
233         Slogf.d(TAG, "Read %d/%d keys from SharedPreferences", keys.size(), numKeys);
234 
235         synchronized (mKeyLock) {
236             mKeys.clear();
237             for (AccountKey key : keys) {
238                 mKeys.add(key);
239             }
240             trimToSize();
241             commit();
242         }
243         return true;
244     }
245 
246     /**
247      * Persists the set of Account Keys to Shared Preferences.
248      *
249      * Account keys are stored in key value pairs of <integer> to <string>, where the integer is the
250      * position in the LRU (higher is more recently used), and the string is a string version of the
251      * bytes. There is also an "AccountKeysCount" preference indicating how many keys are stored.
252      * Keys will have integer keys in the range [0, AccountKeysCount - 1].
253      */
commit()254     private boolean commit() {
255         if (!isUserUnlocked()) {
256             // TODO (243016325): Determine a way to recover from a failed commit()
257             Slogf.w(TAG, "Committed while user was not unlocked. Shared Preferences unavailable");
258             return false;
259         }
260 
261         SharedPreferences preferences =
262                 mContext.getSharedPreferences(FAST_PAIR_PREFERENCES, Context.MODE_PRIVATE);
263         SharedPreferences.Editor editor = preferences.edit();
264 
265         // Get the current count of stored keys
266         int accountKeyCount = preferences.getInt(NUM_ACCOUNT_KEYS, 0);
267         int finalSize = mKeys.size();
268 
269         for (int i = accountKeyCount - 1; i >= finalSize; i--) {
270             editor.remove(Integer.toString(i));
271         }
272 
273         // Add the count of keys
274         editor.putInt(NUM_ACCOUNT_KEYS, finalSize);
275 
276         // Add the keys themselves and apply
277         int i = 0;
278         for (AccountKey key : mKeys) {
279             editor.putString(Integer.toString(i), new BigInteger(key.toBytes()).toString());
280             i++;
281         }
282         editor.apply();
283 
284         if (DBG) {
285             Slogf.d(TAG, "Committed keys to SharedPreferences, keys=%s", mKeys);
286         }
287         return true;
288     }
289 
isUserUnlocked()290     private boolean isUserUnlocked() {
291         return mContext.getSystemService(UserManager.class).isUserUnlocked();
292     }
293 
294     @Override
toString()295     public String toString() {
296         return "FastPairAccountKeyStorage (Size=" + mKeys.size() + " / " + mStorageSize + ")";
297     }
298 
299     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)300     void dump(IndentingPrintWriter writer) {
301         writer.println(toString());
302         writer.increaseIndent();
303         List<AccountKey> keys = getAllAccountKeys();
304         for (AccountKey key : keys) {
305             writer.println("\n" + key);
306         }
307         writer.decreaseIndent();
308     }
309 }
310