1 // Copyright 2017 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink; 18 19 import com.google.crypto.tink.internal.JsonParser; 20 import com.google.crypto.tink.proto.EncryptedKeyset; 21 import com.google.crypto.tink.proto.KeyData; 22 import com.google.crypto.tink.proto.KeyData.KeyMaterialType; 23 import com.google.crypto.tink.proto.KeyStatusType; 24 import com.google.crypto.tink.proto.Keyset; 25 import com.google.crypto.tink.proto.KeysetInfo; 26 import com.google.crypto.tink.proto.OutputPrefixType; 27 import com.google.crypto.tink.subtle.Base64; 28 import com.google.errorprone.annotations.CanIgnoreReturnValue; 29 import com.google.errorprone.annotations.InlineMe; 30 import com.google.gson.JsonArray; 31 import com.google.gson.JsonElement; 32 import com.google.gson.JsonObject; 33 import com.google.gson.JsonParseException; 34 import com.google.protobuf.ByteString; 35 import java.io.ByteArrayInputStream; 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.nio.charset.Charset; 41 import java.nio.file.Path; 42 43 /** 44 * A {@link KeysetReader} that can read from source source cleartext or encrypted keysets in <a 45 * href="https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/JsonFormat">proto 46 * JSON format</a>. 47 * 48 * @since 1.0.0 49 */ 50 public final class JsonKeysetReader implements KeysetReader { 51 private static final Charset UTF_8 = Charset.forName("UTF-8"); 52 53 private final InputStream inputStream; 54 private boolean urlSafeBase64 = false; 55 JsonKeysetReader(InputStream inputStream)56 private JsonKeysetReader(InputStream inputStream) { 57 this.inputStream = inputStream; 58 } 59 60 /** 61 * Static method to create a JsonKeysetReader from an {@link InputStream}. 62 * 63 * <p>Note: the input stream won't be read until {@link JsonKeysetReader#read} or {@link 64 * JsonKeysetReader#readEncrypted} is called. 65 */ 66 @SuppressWarnings("CheckedExceptionNotThrown") withInputStream(InputStream input)67 public static JsonKeysetReader withInputStream(InputStream input) throws IOException { 68 return new JsonKeysetReader(input); 69 } 70 71 /** 72 * Static method to create a JsonKeysetReader from an {@link JsonObject}. 73 * 74 * @deprecated Use {@code #withString} 75 */ 76 @InlineMe( 77 replacement = "JsonKeysetReader.withString(input.toString())", 78 imports = "com.google.crypto.tink.JsonKeysetReader") 79 @Deprecated withJsonObject(Object input)80 public static JsonKeysetReader withJsonObject(Object input) { 81 return withString(input.toString()); 82 } 83 84 /** Static method to create a JsonKeysetReader from a string. */ withString(String input)85 public static JsonKeysetReader withString(String input) { 86 return new JsonKeysetReader(new ByteArrayInputStream(input.getBytes(UTF_8))); 87 } 88 89 /** 90 * Static method to create a JsonKeysetReader from a byte array. 91 * 92 * @deprecated Use TinkJsonProtoKeysetFormat.parseKeyset() instead. 93 */ 94 @Deprecated withBytes(final byte[] bytes)95 public static JsonKeysetReader withBytes(final byte[] bytes) { 96 return new JsonKeysetReader(new ByteArrayInputStream(bytes)); 97 } 98 99 /** 100 * Static method to create a JsonKeysetReader from a file. 101 * 102 * <p>Note: the file won't be read until {@link JsonKeysetReader#read} or {@link 103 * JsonKeysetReader#readEncrypted} is called. 104 * 105 * @deprecated Method should be inlined. 106 */ 107 @InlineMe( 108 replacement = "JsonKeysetReader.withInputStream(new FileInputStream(file))", 109 imports = {"com.google.crypto.tink.JsonKeysetReader", "java.io.FileInputStream"}) 110 @Deprecated withFile(File file)111 public static JsonKeysetReader withFile(File file) throws IOException { 112 return withInputStream(new FileInputStream(file)); 113 } 114 115 /** 116 * Static method to create a JsonKeysetReader from a {@link Path}. 117 * 118 * <p>Note: the file path won't be read until {@link JsonKeysetReader#read} or {@link 119 * JsonKeysetReader#readEncrypted} is called. 120 * 121 * <p>This method only works on Android API level 26 or newer. 122 * 123 * @deprecated Method should be inlined. 124 */ 125 @InlineMe( 126 replacement = "JsonKeysetReader.withInputStream(new FileInputStream(new File(path)))", 127 imports = { 128 "com.google.crypto.tink.JsonKeysetReader", 129 "java.io.File", 130 "java.io.FileInputStream" 131 }) 132 @Deprecated withPath(String path)133 public static JsonKeysetReader withPath(String path) throws IOException { 134 return withInputStream(new FileInputStream(new File(path))); 135 } 136 137 /** 138 * Static method to create a JsonKeysetReader from a {@link Path}. 139 * 140 * <p>Note: the file path won't be read until {@link JsonKeysetReader#read} or {@link 141 * JsonKeysetReader#readEncrypted} is called. 142 * 143 * <p>This method only works on Android API level 26 or newer. 144 * 145 * @deprecated Method should be inlined. 146 */ 147 @InlineMe( 148 replacement = "JsonKeysetReader.withInputStream(new FileInputStream(path.toFile()))", 149 imports = {"com.google.crypto.tink.JsonKeysetReader", "java.io.FileInputStream"}) 150 @Deprecated withPath(Path path)151 public static JsonKeysetReader withPath(Path path) throws IOException { 152 return JsonKeysetReader.withInputStream(new FileInputStream(path.toFile())); 153 } 154 155 @CanIgnoreReturnValue withUrlSafeBase64()156 public JsonKeysetReader withUrlSafeBase64() { 157 this.urlSafeBase64 = true; 158 return this; 159 } 160 161 @Override read()162 public Keyset read() throws IOException { 163 try { 164 return keysetFromJson( 165 JsonParser.parse(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject()); 166 } catch (JsonParseException | IllegalStateException e) { 167 throw new IOException(e); 168 } finally { 169 if (inputStream != null) { 170 inputStream.close(); 171 } 172 } 173 } 174 175 @Override readEncrypted()176 public EncryptedKeyset readEncrypted() throws IOException { 177 try { 178 return encryptedKeysetFromJson( 179 JsonParser.parse(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject()); 180 } catch (JsonParseException | IllegalStateException e) { 181 throw new IOException(e); 182 } finally { 183 if (inputStream != null) { 184 inputStream.close(); 185 } 186 } 187 } 188 189 private static final long MAX_KEY_ID = 4294967295L; // = 2^32 - 1 190 private static final long MIN_KEY_ID = Integer.MIN_VALUE; // = - 2^31 191 192 /** 193 * getKeyId parses an element into a 32-bit integer. If the element does not contain a number or 194 * the number is not in the range of either a signed or an unsigned 32-bit integer, it throws an 195 * IOException. If the number is in the range [2^31, 2^32-1], it is cast into a negative integer. 196 */ getKeyId(JsonElement element)197 private static int getKeyId(JsonElement element) throws IOException { 198 if (!element.isJsonPrimitive()) { 199 throw new IOException("invalid key id: not a JSON primitive"); 200 } 201 if (!element.getAsJsonPrimitive().isNumber()) { 202 throw new IOException("invalid key id: not a JSON number"); 203 } 204 Number number = element.getAsJsonPrimitive().getAsNumber(); 205 long id; 206 try { 207 id = JsonParser.getParsedNumberAsLongOrThrow(number); 208 } catch (NumberFormatException e) { 209 throw new IOException(e); 210 } 211 if (id > MAX_KEY_ID || id < MIN_KEY_ID) { 212 throw new IOException("invalid key id"); 213 } 214 // casts large unsigned int32 numbers to negative int32 numbers 215 return (int) id; 216 } 217 keysetFromJson(JsonObject json)218 private Keyset keysetFromJson(JsonObject json) throws IOException { 219 if (!json.has("key")) { 220 throw new JsonParseException("invalid keyset: no key"); 221 } 222 JsonElement key = json.get("key"); 223 if (!key.isJsonArray()) { 224 throw new JsonParseException("invalid keyset: key must be an array"); 225 } 226 JsonArray keys = key.getAsJsonArray(); 227 if (keys.size() == 0) { 228 throw new JsonParseException("invalid keyset: key is empty"); 229 } 230 Keyset.Builder builder = Keyset.newBuilder(); 231 if (json.has("primaryKeyId")) { 232 builder.setPrimaryKeyId(getKeyId(json.get("primaryKeyId"))); 233 } 234 for (int i = 0; i < keys.size(); i++) { 235 builder.addKey(keyFromJson(keys.get(i).getAsJsonObject())); 236 } 237 return builder.build(); 238 } 239 encryptedKeysetFromJson(JsonObject json)240 private EncryptedKeyset encryptedKeysetFromJson(JsonObject json) throws IOException { 241 validateEncryptedKeyset(json); 242 byte[] encryptedKeyset; 243 if (urlSafeBase64) { 244 encryptedKeyset = Base64.urlSafeDecode(json.get("encryptedKeyset").getAsString()); 245 } else { 246 encryptedKeyset = Base64.decode(json.get("encryptedKeyset").getAsString()); 247 } 248 if (json.has("keysetInfo")) { 249 return EncryptedKeyset.newBuilder() 250 .setEncryptedKeyset(ByteString.copyFrom(encryptedKeyset)) 251 .setKeysetInfo(keysetInfoFromJson(json.getAsJsonObject("keysetInfo"))) 252 .build(); 253 } else { 254 return EncryptedKeyset.newBuilder() 255 .setEncryptedKeyset(ByteString.copyFrom(encryptedKeyset)) 256 .build(); 257 } 258 } 259 keyFromJson(JsonObject json)260 private Keyset.Key keyFromJson(JsonObject json) throws IOException { 261 if (!json.has("keyData") 262 || !json.has("status") 263 || !json.has("keyId") 264 || !json.has("outputPrefixType")) { 265 throw new JsonParseException("invalid key"); 266 } 267 JsonElement keyData = json.get("keyData"); 268 if (!keyData.isJsonObject()) { 269 throw new JsonParseException("invalid key: keyData must be an object"); 270 } 271 return Keyset.Key.newBuilder() 272 .setStatus(getStatus(json.get("status").getAsString())) 273 .setKeyId(getKeyId(json.get("keyId"))) 274 .setOutputPrefixType(getOutputPrefixType(json.get("outputPrefixType").getAsString())) 275 .setKeyData(keyDataFromJson(keyData.getAsJsonObject())) 276 .build(); 277 } 278 keysetInfoFromJson(JsonObject json)279 private static KeysetInfo keysetInfoFromJson(JsonObject json) throws IOException { 280 KeysetInfo.Builder builder = KeysetInfo.newBuilder(); 281 if (json.has("primaryKeyId")) { 282 builder.setPrimaryKeyId(getKeyId(json.get("primaryKeyId"))); 283 } 284 if (json.has("keyInfo")) { 285 JsonArray keyInfos = json.getAsJsonArray("keyInfo"); 286 for (int i = 0; i < keyInfos.size(); i++) { 287 builder.addKeyInfo(keyInfoFromJson(keyInfos.get(i).getAsJsonObject())); 288 } 289 } 290 return builder.build(); 291 } 292 keyInfoFromJson(JsonObject json)293 private static KeysetInfo.KeyInfo keyInfoFromJson(JsonObject json) throws IOException { 294 return KeysetInfo.KeyInfo.newBuilder() 295 .setStatus(getStatus(json.get("status").getAsString())) 296 .setKeyId(getKeyId(json.get("keyId"))) 297 .setOutputPrefixType(getOutputPrefixType(json.get("outputPrefixType").getAsString())) 298 .setTypeUrl(json.get("typeUrl").getAsString()) 299 .build(); 300 } 301 keyDataFromJson(JsonObject json)302 private KeyData keyDataFromJson(JsonObject json) { 303 if (!json.has("typeUrl") || !json.has("value") || !json.has("keyMaterialType")) { 304 throw new JsonParseException("invalid keyData"); 305 } 306 byte[] value; 307 if (urlSafeBase64) { 308 value = Base64.urlSafeDecode(json.get("value").getAsString()); 309 } else { 310 value = Base64.decode(json.get("value").getAsString()); 311 } 312 return KeyData.newBuilder() 313 .setTypeUrl(json.get("typeUrl").getAsString()) 314 .setValue(ByteString.copyFrom(value)) 315 .setKeyMaterialType(getKeyMaterialType(json.get("keyMaterialType").getAsString())) 316 .build(); 317 } 318 getStatus(String status)319 private static KeyStatusType getStatus(String status) { 320 switch (status) { 321 case "ENABLED": 322 return KeyStatusType.ENABLED; 323 case "DISABLED": 324 return KeyStatusType.DISABLED; 325 case "DESTROYED": 326 return KeyStatusType.DESTROYED; 327 default: 328 throw new JsonParseException("unknown status: " + status); 329 } 330 } 331 getOutputPrefixType(String type)332 private static OutputPrefixType getOutputPrefixType(String type) { 333 switch (type) { 334 case "TINK": 335 return OutputPrefixType.TINK; 336 case "RAW": 337 return OutputPrefixType.RAW; 338 case "LEGACY": 339 return OutputPrefixType.LEGACY; 340 case "CRUNCHY": 341 return OutputPrefixType.CRUNCHY; 342 default: 343 throw new JsonParseException("unknown output prefix type: " + type); 344 } 345 } 346 getKeyMaterialType(String type)347 private static KeyMaterialType getKeyMaterialType(String type) { 348 switch (type) { 349 case "SYMMETRIC": 350 return KeyMaterialType.SYMMETRIC; 351 case "ASYMMETRIC_PRIVATE": 352 return KeyMaterialType.ASYMMETRIC_PRIVATE; 353 case "ASYMMETRIC_PUBLIC": 354 return KeyMaterialType.ASYMMETRIC_PUBLIC; 355 case "REMOTE": 356 return KeyMaterialType.REMOTE; 357 default: 358 throw new JsonParseException("unknown key material type: " + type); 359 } 360 } 361 validateEncryptedKeyset(JsonObject json)362 private static void validateEncryptedKeyset(JsonObject json) { 363 if (!json.has("encryptedKeyset")) { 364 throw new JsonParseException("invalid encrypted keyset"); 365 } 366 } 367 368 } 369