• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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