Skip to content

Report malformed JSON keyset fields as IOException instead of an uncaught exception#70

Merged
copybara-service[bot] merged 2 commits into
tink-crypto:mainfrom
adilburaksen:fix/jsonkeysetreader-malformed-field-exception
Jul 22, 2026
Merged

Report malformed JSON keyset fields as IOException instead of an uncaught exception#70
copybara-service[bot] merged 2 commits into
tink-crypto:mainfrom
adilburaksen:fix/jsonkeysetreader-malformed-field-exception

Conversation

@adilburaksen

Copy link
Copy Markdown
Contributor

Problem

JsonKeysetReader.read() and readEncrypted() declare throws IOException, and the public TinkJsonProtoKeysetFormat.parseKeyset* helpers wrap that into throws GeneralSecurityException. Their try blocks catch JsonParseException | IllegalStateException and rethrow as IOException.

However, 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. That is a RuntimeException and is not covered by the existing catch, so it propagates uncaught, past the documented IOException / GeneralSecurityException contract.

A caller that parses an untrusted keyset (for example a public keyset via parseKeysetWithoutSecret) and handles only the declared exceptions will therefore see an unexpected UnsupportedOperationException instead of a clean parse error.

Minimal reproducer (the field is a {} instead of a string):

{"primaryKeyId":1,"key":[{"keyData":{"typeUrl":{},"value":"","keyMaterialType":"SYMMETRIC"},"status":"ENABLED","keyId":1,"outputPrefixType":"TINK"}]}

getAsString() on the object-valued typeUrl throws UnsupportedOperationException. (The existing testRead_outputPrefixTypeIsNotAString test uses a number, which gson coerces to a string, so the object/array case is not currently exercised.)

Fix

Add UnsupportedOperationException to the catch clauses in read() and readEncrypted() so malformed input is reported as IOException, consistent with the other parse errors.

} catch (JsonParseException | IllegalStateException | UnsupportedOperationException e) {
  throw new IOException(e);
}

Happy to add a test covering object/array-valued string fields if preferred.

…ught 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.
@adilburaksen

Copy link
Copy Markdown
Contributor Author

@morambro could you take a look? Parsing a malformed JSON keyset currently throws an uncaught runtime exception instead of a clean IOException, so callers can't catch it the way the API implies. The change reports it as IOException. Small and covered by a test — let me know if you'd prefer a different exception type.

@morambro
morambro requested a review from tholenst July 21, 2026 08:44

@tholenst tholenst left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test which would have failed before this patch?

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.
@adilburaksen

Copy link
Copy Markdown
Contributor Author

@tholenst added in 30002a3. Two tests — one on the read() path (status as an object) and one on readEncrypted() (encryptedKeyset as an object) — each hitting a field that's expected to be a primitive so gson's getAsString() raises UnsupportedOperationException during parsing. I checked both directions locally: against the pre-patch code they fail with expected IOException but was UnsupportedOperationException, and they pass once it's wrapped. Let me know if you'd like a different malformed shape covered.

@copybara-service
copybara-service Bot merged commit 7d2c417 into tink-crypto:main Jul 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants