-
Notifications
You must be signed in to change notification settings - Fork 295
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added SHL Decoding Interfaces and Implementations (#2434)
* Added interfaces and utils needed for decoding * Separated decoderImpl into smaller functions and added more tests for readShlUtils * Started implementing tests for decoderImpl * Finished unit tests for ReadSHLinkUtils * Wrote test for SHLinkDecoder * Added comments and cleaned code * More DecoderImpl unit tests added * Ran gradle checks * Removed constructShl function from impl and SHLScanDataInput from Decoder parameters - refactored tests to acommodate this * Changed test names to use backticks to make them more readable * Migrating asserts to the Truth library * Migrated all decoding tests to use Truth library * Added helpful comments to DecoderImpl * Added kdoc to the Decoder interface * Changed Base64 library from java to android - so minSdk can be kept at 24 * Changed Retrofit object variable name to be more descriptive * Extracted common variables in decoderImpl tests * Changed shLinkScanData create function to be inside the companion object and removed public constructors * Improved kdoc in interface * Refactored decodeSHLink to take in passcode and recipient as separate strings - instead of a predefined JSON object * Ran gradle checks * Correct the IPSDocument create function * Added named parameter in IPSDocument create function * Changed tests to use assertThrows and improved decoder tests --------- Co-authored-by: aditya-07 <[email protected]>
- Loading branch information
1 parent
70fd2ae
commit d3841eb
Showing
11 changed files
with
919 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
document/src/main/java/com.google.android.fhir.document/decode/ReadSHLinkUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/* | ||
* Copyright 2024 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.google.android.fhir.document.decode | ||
|
||
import android.util.Base64 | ||
import com.nimbusds.jose.JWEDecrypter | ||
import com.nimbusds.jose.JWEObject | ||
import com.nimbusds.jose.crypto.DirectDecrypter | ||
import java.io.ByteArrayOutputStream | ||
import java.util.zip.DataFormatException | ||
import java.util.zip.Inflater | ||
import org.json.JSONObject | ||
|
||
object ReadSHLinkUtils { | ||
|
||
/* Extracts the part of the link after the 'shlink:/' */ | ||
fun extractUrl(scannedData: String): String { | ||
if (scannedData.contains("shlink:/")) { | ||
return scannedData.substringAfterLast("shlink:/") | ||
} | ||
throw IllegalArgumentException("Not a valid SHLink") | ||
} | ||
|
||
/* Decodes the extracted url from Base64Url to a byte array */ | ||
fun decodeUrl(extractedUrl: String): ByteArray { | ||
if (extractedUrl.isEmpty()) { | ||
throw IllegalArgumentException("Not a valid Base64 encoded string") | ||
} | ||
try { | ||
return Base64.decode(extractedUrl.toByteArray(), Base64.URL_SAFE) | ||
} catch (err: IllegalArgumentException) { | ||
throw IllegalArgumentException("Not a valid Base64 encoded string") | ||
} | ||
} | ||
|
||
/* Returns a string of data found in the verifiableCredential field in the given JSON */ | ||
fun extractVerifiableCredential(jsonString: String): String { | ||
val jsonObject = JSONObject(jsonString) | ||
if (jsonObject.has("verifiableCredential")) { | ||
val verifiableCredentialArray = jsonObject.getJSONArray("verifiableCredential") | ||
|
||
if (verifiableCredentialArray.length() > 0) { | ||
// Assuming you want the first item from the array | ||
return verifiableCredentialArray.getString(0) | ||
} | ||
} | ||
return "" | ||
} | ||
|
||
/* Decodes and decompresses the payload in a JWT token */ | ||
fun decodeAndDecompressPayload(token: String): String { | ||
try { | ||
val tokenParts = token.split('.') | ||
if (tokenParts.size < 2) { | ||
throw Error("Invalid JWT token passed in") | ||
} | ||
val decoded = Base64.decode(tokenParts[1], Base64.URL_SAFE) | ||
val inflater = Inflater(true) | ||
inflater.setInput(decoded) | ||
val initialBufferSize = 100000 | ||
val decompressedBytes = ByteArrayOutputStream(initialBufferSize) | ||
val buffer = ByteArray(8192) | ||
|
||
try { | ||
while (!inflater.finished()) { | ||
val length = inflater.inflate(buffer) | ||
decompressedBytes.write(buffer, 0, length) | ||
} | ||
decompressedBytes.close() | ||
} catch (e: DataFormatException) { | ||
throw Error("$e.printStackTrace()") | ||
} | ||
inflater.end() | ||
return decompressedBytes.toByteArray().decodeToString() | ||
} catch (err: Error) { | ||
throw Error("Invalid JWT token passed in: $err") | ||
} | ||
} | ||
|
||
/* Decodes and decompresses the embedded health data from a JWE token into a string */ | ||
fun decodeShc(responseBody: String, key: String): String { | ||
try { | ||
if (responseBody.isEmpty() or key.isEmpty()) { | ||
throw IllegalArgumentException("The provided strings should not be empty") | ||
} | ||
val jweObject = JWEObject.parse(responseBody) | ||
val decodedKey: ByteArray = Base64.decode(key, Base64.URL_SAFE) | ||
val decrypter: JWEDecrypter = DirectDecrypter(decodedKey) | ||
jweObject.decrypt(decrypter) | ||
return jweObject.payload.toString() | ||
} catch (err: Exception) { | ||
throw Exception("JWE decryption failed: $err") | ||
} | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
document/src/main/java/com.google.android.fhir.document/decode/SHLinkDecoder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/* | ||
* Copyright 2024 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.google.android.fhir.document.decode | ||
|
||
import com.google.android.fhir.document.IPSDocument | ||
|
||
/** | ||
* The [SHLinkDecoder] interface defines a contract for decoding Smart Health Links (SHLs) into | ||
* [IPSDocument] objects. Implementations of this interface are responsible for decoding and | ||
* decompressing SHLs, fetching associated health data from external sources, and creating | ||
* IPSDocument instances. | ||
* | ||
* The process of decoding SHLs is outlined in its documentation | ||
* [SHL Documentation](https://docs.smarthealthit.org/smart-health-links/). | ||
* | ||
* ## Example Decoding Process: | ||
* A SHL is formatted as `[optionalViewer]shlink:/[Base64-Encoded Payload]` (e.g., | ||
* `shlink:/eyJsYWJ...`). First, extract the portion of the link after 'shlink:/' and decode this to | ||
* give a SHL Payload. SHL Payloads are structured as: | ||
* ``` | ||
* { | ||
* "url": manifest url, | ||
* "key": SHL-specific key, | ||
* "label": "2023-07-12", | ||
* "flag": "LPU", | ||
* "exp": expiration time, | ||
* "v": SHL Protocol Version | ||
* } | ||
* ``` | ||
* | ||
* The label, flag, exp, and v properties are optional. | ||
* | ||
* Send a POST request to the manifest URL with a header of "Content-Type":"application/json" and a | ||
* body with a "Recipient", a "Passcode" if the "P" flag is present and optionally | ||
* "embeddedLengthMax":INT. Example request body: | ||
* | ||
* ``` | ||
* { | ||
* "recipient" : "example_name", | ||
* "passcode" : "example_passcode" | ||
* } | ||
* ``` | ||
* ``` | ||
* | ||
* If the POST request is successful, a list of files is returned. | ||
* Example response: | ||
* | ||
* ``` | ||
* | ||
* { "files" : | ||
* [ { "contentType": "application/smart-health-card", "location":"https://bucket.cloud.example..." }, { "contentType": "application/smart-health-card", "embedded":"eyJhb..." } ] | ||
* } | ||
* | ||
* ``` | ||
* | ||
* A file can be one of two types: | ||
* - Location: If the resource is stored in a location, a single GET request can be made to retrieve the data. | ||
* - Embedded: If the file type is embedded, the data is a JWE token which can be decoded with the SHL-specific key. | ||
*/ | ||
interface SHLinkDecoder { | ||
|
||
/** | ||
* Decodes and decompresses a Smart Health Link (SHL) into an [IPSDocument] object. | ||
* | ||
* @param shLink The full Smart Health Link. | ||
* @param recipient The recipient for the manifest request. | ||
* @param passcode The passcode for the manifest request (optional, will be null if the P flag is | ||
* not present in the SHL payload). | ||
* @return An [IPSDocument] object if decoding is successful, otherwise null. | ||
*/ | ||
suspend fun decodeSHLinkToDocument( | ||
shLink: String, | ||
recipient: String, | ||
passcode: String?, | ||
): IPSDocument? | ||
} |
Oops, something went wrong.