1 /*
2  * Copyright (C) 2018 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.bips.ipp;
18 
19 import android.util.JsonReader;
20 import android.util.JsonWriter;
21 import android.util.Log;
22 
23 import com.android.bips.BuiltInPrintService;
24 
25 import java.io.BufferedReader;
26 import java.io.BufferedWriter;
27 import java.io.File;
28 import java.io.FileReader;
29 import java.io.FileWriter;
30 import java.io.IOException;
31 import java.util.Arrays;
32 import java.util.HashMap;
33 import java.util.Map;
34 
35 /**
36  * A persistent cache of certificate public keys known to be associated with certain printer
37  * UUIDs.
38  */
39 public class CertificateStore {
40     private static final String TAG = CertificateStore.class.getSimpleName();
41     private static final boolean DEBUG = false;
42 
43     /** File location of the on-disk certificate store. */
44     private final File mStoreFile;
45 
46     /** RAM-based store of certificates (UUID to certificate) */
47     private final Map<String, byte[]> mCertificates = new HashMap<>();
48 
CertificateStore(BuiltInPrintService service)49     public CertificateStore(BuiltInPrintService service) {
50         mStoreFile = new File(service.getCacheDir(), getClass().getSimpleName() + ".json");
51         load();
52     }
53 
54     /** Write a new, non-null certificate to the store. */
put(String uuid, byte[] certificate)55     public void put(String uuid, byte[] certificate) {
56         byte[] oldCertificate = mCertificates.put(uuid, certificate);
57         if (oldCertificate == null || !Arrays.equals(oldCertificate, certificate)) {
58             // Cache the certificate for later
59             if (DEBUG) Log.d(TAG, "New certificate uuid=" + uuid + " len=" + certificate.length);
60             save();
61         }
62     }
63 
64     /** Remove any certificate associated with the specified UUID. */
remove(String uuid)65     public void remove(String uuid) {
66         if (mCertificates.remove(uuid) != null) {
67             save();
68         }
69     }
70 
71     /** Return the known certificate public key for a printer having the specified UUID, or null. */
get(String uuid)72     public byte[] get(String uuid) {
73         return mCertificates.get(uuid);
74     }
75 
76     /** Write to storage immediately. */
save()77     private void save() {
78         if (mStoreFile.exists()) {
79             mStoreFile.delete();
80         }
81 
82         try (JsonWriter writer = new JsonWriter(new BufferedWriter(new FileWriter(mStoreFile)))) {
83             writer.beginObject();
84             writer.name("certificates");
85             writer.beginArray();
86             for (Map.Entry<String, byte[]> entry : mCertificates.entrySet()) {
87                 writer.beginObject();
88                 writer.name("uuid").value(entry.getKey());
89                 writer.name("pubkey").value(bytesToHex(entry.getValue()));
90                 writer.endObject();
91             }
92             writer.endArray();
93             writer.endObject();
94             if (DEBUG) Log.d(TAG, "Wrote " + mCertificates.size() + " certificates to store");
95         } catch (NullPointerException | IOException e) {
96             Log.w(TAG, "Error while storing to " + mStoreFile, e);
97         }
98     }
99 
100     /** Load known certificates from storage into RAM. */
load()101     private void load() {
102         if (!mStoreFile.exists()) {
103             return;
104         }
105 
106         try (JsonReader reader = new JsonReader(new BufferedReader(new FileReader(mStoreFile)))) {
107             reader.beginObject();
108             while (reader.hasNext()) {
109                 String itemName = reader.nextName();
110                 if (itemName.equals("certificates")) {
111                     reader.beginArray();
112                     while (reader.hasNext()) {
113                         loadItem(reader);
114                     }
115                     reader.endArray();
116                 } else {
117                     reader.skipValue();
118                 }
119             }
120             reader.endObject();
121         } catch (IllegalStateException | IOException error) {
122             Log.w(TAG, "Error while loading from " + mStoreFile, error);
123         }
124         if (DEBUG) Log.d(TAG, "Loaded size=" + mCertificates.size() + " from " + mStoreFile);
125     }
126 
127     /** Load a single certificate entry into RAM. */
loadItem(JsonReader reader)128     private void loadItem(JsonReader reader) throws IOException {
129         String uuid = null;
130         byte[] pubkey = null;
131         reader.beginObject();
132         while (reader.hasNext()) {
133             String itemName = reader.nextName();
134             switch(itemName) {
135                 case "uuid":
136                     uuid = reader.nextString();
137                     break;
138                 case "pubkey":
139                     try {
140                         pubkey = hexToBytes(reader.nextString());
141                     } catch (IllegalArgumentException ignored) {
142                     }
143                     break;
144                 default:
145                     reader.skipValue();
146             }
147         }
148         reader.endObject();
149         if (uuid != null && pubkey != null) {
150             mCertificates.put(uuid, pubkey);
151         }
152     }
153 
154     private static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
155 
156     /** Converts a byte array to a hexadecimal string, or null if bytes are null. */
bytesToHex(byte[] bytes)157     private static String bytesToHex(byte[] bytes) {
158         if (bytes == null) {
159             return null;
160         }
161 
162         char[] hexChars = new char[bytes.length * 2];
163         for (int i = 0; i < bytes.length; i++) {
164             int b = bytes[i] & 0xFF;
165             hexChars[i * 2] = HEX_CHARS[b >>> 4];
166             hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F];
167         }
168         return new String(hexChars);
169     }
170 
171     /** Converts a hexadecimal string to a byte array, or null if hexString is null. */
hexToBytes(String hexString)172     private static byte[] hexToBytes(String hexString) {
173         if (hexString == null) {
174             return null;
175         }
176 
177         char[] source = hexString.toCharArray();
178         byte[] dest = new byte[source.length / 2];
179         for (int sourcePos = 0, destPos = 0; sourcePos < source.length; ) {
180             int hi = Character.digit(source[sourcePos++], 16);
181             int lo = Character.digit(source[sourcePos++], 16);
182             if ((hi < 0) || (lo < 0)) {
183                 throw new IllegalArgumentException();
184             }
185             dest[destPos++] = (byte) (hi << 4 | lo);
186         }
187         return dest;
188     }
189 }
190