From d9552c823ce0c3d86ce7c282885800363e63d2e4 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Tue, 9 Jun 2026 22:47:24 +0300 Subject: [PATCH 1/2] Report malformed JSON keyset fields as IOException instead of an uncaught exception JsonKeysetReader.read()/readEncrypted() declare `throws IOException`, and the public TinkJsonProtoKeysetFormat.parseKeyset* helpers wrap that into `throws GeneralSecurityException`. When a JSON keyset sets a string-typed field (e.g. "status", "typeUrl", "keyMaterialType", "value", "encryptedKeyset") to a JSON object or array, gson's JsonElement.getAsString() throws UnsupportedOperationException, a RuntimeException not covered by the existing `catch (JsonParseException | IllegalStateException)`. It then propagates uncaught, past the documented IOException / GeneralSecurityException contract, so a caller parsing an untrusted (e.g. public) keyset that only handles the declared exceptions crashes. Add UnsupportedOperationException to the catch clauses so malformed input is reported as IOException, consistent with other parse errors. --- src/main/java/com/google/crypto/tink/JsonKeysetReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/crypto/tink/JsonKeysetReader.java b/src/main/java/com/google/crypto/tink/JsonKeysetReader.java index 2bd68988b..3baa18085 100644 --- a/src/main/java/com/google/crypto/tink/JsonKeysetReader.java +++ b/src/main/java/com/google/crypto/tink/JsonKeysetReader.java @@ -163,7 +163,7 @@ public Keyset read() throws IOException { try { return keysetFromJson( JsonParser.parse(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject()); - } catch (JsonParseException | IllegalStateException e) { + } catch (JsonParseException | IllegalStateException | UnsupportedOperationException e) { throw new IOException(e); } finally { if (inputStream != null) { @@ -177,7 +177,7 @@ public EncryptedKeyset readEncrypted() throws IOException { try { return encryptedKeysetFromJson( JsonParser.parse(new String(Util.readAll(inputStream), UTF_8)).getAsJsonObject()); - } catch (JsonParseException | IllegalStateException e) { + } catch (JsonParseException | IllegalStateException | UnsupportedOperationException e) { throw new IOException(e); } finally { if (inputStream != null) { From 30002a38a8f9cd7e2d2281247b7a679749a7b4ec Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Tue, 21 Jul 2026 12:06:50 +0300 Subject: [PATCH 2/2] Add regression tests for non-primitive JSON keyset fields Cover the read() and readEncrypted() paths where a field that is expected to be a JSON primitive is instead an object, which makes gson's getAsString() raise UnsupportedOperationException. Both tests fail against the previous code (the exception escaped as UnsupportedOperationException) and pass now that it is wrapped in an IOException. --- .../crypto/tink/JsonKeysetReaderTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java b/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java index 11e894a89..de44eae5b 100644 --- a/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java +++ b/src/test/java/com/google/crypto/tink/JsonKeysetReaderTest.java @@ -468,6 +468,25 @@ public void testReadKeyset_keyIdWithComment_throws() throws Exception { assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read()); } + @Test + public void readKeyset_nonPrimitiveStatusField_throwsIOException() throws Exception { + // "status" is a JSON object, so gson's getAsString() raises UnsupportedOperationException while + // parsing the key. read() must surface that as an IOException rather than let it escape + // uncaught to the caller. + String jsonKeysetString = + "{\"key\":[{\"keyData\":{},\"status\":{},\"keyId\":1,\"outputPrefixType\":\"TINK\"}]}"; + assertThrows(IOException.class, () -> JsonKeysetReader.withString(jsonKeysetString).read()); + } + + @Test + public void readEncrypted_nonPrimitiveEncryptedKeysetField_throwsIOException() throws Exception { + // "encryptedKeyset" is a JSON object, so getAsString() raises UnsupportedOperationException. + // readEncrypted() must surface that as an IOException rather than let it escape uncaught. + String jsonEncryptedKeyset = "{\"encryptedKeyset\":{}}"; + assertThrows( + IOException.class, () -> JsonKeysetReader.withString(jsonEncryptedKeyset).readEncrypted()); + } + @Test public void testReadKeyset_withDuplicatedMapKey_throws() throws Exception { String jsonKeysetString = "{"