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