Initial commit

main
Bas Kloosterman 1 year ago
commit 377dc21ddd
  1. 109
      .gitignore
  2. 3
      .idea/.gitignore
  3. 1
      .idea/.name
  4. 9
      .idea/TestProject.iml
  5. 19
      .idea/artifacts/WbxTokenSigner_main_jar.xml
  6. 10
      .idea/codeStyles/Project.xml
  7. 5
      .idea/codeStyles/codeStyleConfig.xml
  8. 6
      .idea/compiler.xml
  9. 20
      .idea/jarRepositories.xml
  10. 6
      .idea/kotlinc.xml
  11. 7
      .idea/misc.xml
  12. 10
      .idea/runConfigurations.xml
  13. 45
      build.gradle.kts
  14. 361
      docs/readme.html
  15. 130
      docs/readme.md
  16. BIN
      docs/readme.pdf
  17. 39
      dvpa-r-data/token_template.json
  18. 1
      gradle.properties
  19. BIN
      gradle/wrapper/gradle-wrapper.jar
  20. 5
      gradle/wrapper/gradle-wrapper.properties
  21. 185
      gradlew
  22. 89
      gradlew.bat
  23. BIN
      lib/cid-sdk-java.jar
  24. BIN
      lib/cid-sdk-zorg-id.jar
  25. 0
      logs/.keep
  26. 3
      settings.gradle.kts
  27. 46
      src/main/kotlin/DVPARTokenTemplate.kt
  28. 224
      src/main/kotlin/Main.kt
  29. 171
      src/main/kotlin/WBXAuthToken.kt
  30. 7
      src/main/resources/META-INF/MANIFEST.MF

109
.gitignore vendored

@ -0,0 +1,109 @@
# From https://github.com/github/gitignore/blob/master/Gradle.gitignore
.gradle
/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# From https://github.com/github/gitignore/blob/master/Java.gitignore
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# From https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
logs/*
!logs/.keep

3
.idea/.gitignore vendored

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

@ -0,0 +1 @@
WbxTokenSigner

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,19 @@
<component name="ArtifactManager">
<artifact build-on-make="true" name="WbxTokenSigner.main:jar">
<output-path>$PROJECT_DIR$/out/artifacts/WbxTokenSigner_main_jar</output-path>
<root id="root">
<element id="archive" name="WbxTokenSigner.main.jar">
<element id="module-output" name="WbxTokenSigner.main" />
</element>
<element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib:1.5.31" />
<element id="file-copy" path="$PROJECT_DIR$/lib/cid-sdk-zorg-id.jar" />
<element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31" />
<element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31" />
<element id="library" level="project" name="Gradle: org.jetbrains:annotations:13.0" />
<element id="library" level="project" name="Gradle: org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.3.0" />
<element id="library" level="project" name="Gradle: org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.3.0" />
<element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31" />
<element id="file-copy" path="$PROJECT_DIR$/lib/cid-sdk-java.jar" />
</root>
</artifact>
</component>

@ -0,0 +1,10 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="16" />
</component>
</project>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.6.21" />
</component>
</project>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" default="true" project-jdk-name="semeru-16" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

@ -0,0 +1,45 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.5.31"
application
kotlin("plugin.serialization") version "1.5.31"
}
group = "me.bas"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
//tasks.jar {
// manifest {
// attributes["Main-Class"] = "MainKt"
// }
// configurations["compileClasspath"].forEach { file: File ->
// from(zipTree(file.absoluteFile))
// }
//
// duplicatesStrategy = DuplicatesStrategy.EXCLUDE
//}
dependencies {
testImplementation(kotlin("test"))
implementation(files("./lib/cid-sdk-java.jar"))
implementation(files("./lib/cid-sdk-zorg-id.jar"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")
}
tasks.test {
useJUnit()
}
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "1.8"
}
application {
mainClass.set("MainKt")
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,130 @@
# Whitebox token authenticatie in Topicus HAP
Om de integratie van Whitebox token authenticatie in Topicus HAP mogelijk te maken hebben wij een manier bedacht om de huidige token authenticatie ook praktisch te kunnen gebruiken binnen een webapplicatie draaiende in een browser. Hierin worden de Whitebox authenticatie tokens ondertekend middels ZorgID. Om de inspanning van het implementatie traject aan de kant van Topicus HAP te beperken hebben wij een voorbeeld implementatie geschreven in Kotlin. Deze kan gedeeltelijk gebruikt worden as is, of als voorbeeld dienen voor de daadwerkelijke implementatie.
## Onderdelen
Dit project bevat twee onderdelen
1) WBXAuthToken class (WBXAuthtoken.kt)
De WBXAuthToken class representeert een authenticatie token zoals de Whitebox deze ondersteund.
De class stelt methodes beschikbaar om een authenticatie token te coderen tot een formaat dat de ZorgID SDK kan ondertekenen en een token in het juiste formaat (JWS) kan formateren zodat een Whitebox / DVPA-r module deze kan gebruiken/valideren.
2) Voorbeeld applicatie (Main.kt)
De voorbeeld applicatie demonstreert het proces van het genereren en verwerken van een authenticatie token voor zowel de iFrame flow als de DVPA-r flow.
## WBXAuthToken class API
De constructor van de WBXAuthToken class neemt 5 argumenten waarvan 1 optioneel (system_key_fp): de Whitebox URL, de bijbehorende challenge, de BSN van de op te vragen patiënt, en het UZI certificaat waarmee het token ondertekend gaat worden. Als laatste argument kan de fingerprint van het systeem certificaat worden meegegeven, dit is alleen van toepassing als men de de DVPA-r methode gebruikt. Deze fingerprint wordt aangeleverd in in de DVPA-r token template. Voor details hierover kunt u terecht in in de [DVPA-r documentatie](https://docs.mcsr.nl/dvpa-r).
Daarnaast heeft de WBXAuthToken class 6 publieke methodes die het proces vormgeven.
**1) updateTimestamp**
Met deze methode wordt de timestamp van het token (de timestamp bepaald de uiteindelijke geldigheid van het token) naar de huidige datum en tijd ingesteld. Deze methode wordt aangeroepen bij het initialiseren van het token. Het is daarom niet nodig om deze zelf aan te roepen. Het is echter wel mogelijk (en aan te raden) om de timestamp te updaten indien er veel tijd zit tussen het initialiseren van het token en het daadwerkelijk ondertekenen van het token. **Let op!** als de data voor ondertekening is opgehaald met de getHashBag methode moet de updateTimestamp methode niet meer worden aangeroepen. De timestamp is onderdeel van de data die wordt ondertekend dus als deze methode wordt aangeroepen na ondertekening wordt de uiteindelijk gegenereerde JWS ongeldig. Dit wordt momenteel niet geforceerd in de class zelf.
**2) getHashBag**
Deze methode geeft de data terug die ondertekend moet worden, gewrapped in een *SignDataRequestBag*. Deze *SignDataRequestBag* kan aan de sign methode van de transaction manager (onderdeel van de ZorgID SDK) worden meegeven om te worden ondertekend.
**3) convertAndSetSignature**
Deze methode converteert de handtekening naar het juiste formaat en slaat deze op voor later gebruik.
De handtekening die terug gegeven wordt vanuit de ZorgID SDK is gecodeerd in standaard base64 formaat. In de standaard die de Whitebox authenicatie token implementeerd ([JWS](https://datatracker.ietf.org/doc/html/rfc7515)) wordt een URL base64 encoding scheme gebruikt zonder padding.
**4) jws**
Deze methode geeft het Whitebox authenticatie token terug in het [JWS compact formaat](https://datatracker.ietf.org/doc/html/rfc7515#section-3.1). Dit is het formaat dat de Whitebox verwacht.
**5) url**
Deze methode geeft de Whitebox URL terug waarvoor deze Whitebox authenticatie token toegang geeft.
**6) endpoint**
Deze methode geeft het endpoint (URL) terug waarnaar het token en url moeten worden opgestuurd (d.m.v een HTTP POST request). Deze methode is alleen van toepassing op de iFrame variant.
## Whitebox token authenticatie proces
Naast de TLS sessie authenticatie, die momenteel gebruikt wordt door Topicus HAP, biedt de Whitebox ook een token authenticatie methode aan. Deze methode is op een relatief simple manier te implementeren binnen Topicus HAP met behulp van de ZorgID SDK/applicatie en de WBXAuthToken class.
Om het proces te laten werken is er naast alleen de Whitebox URL (zoals in de huidige implementatie) ook een zogenaamde challenge nodig. Deze wordt verkregen bij het opvragen van de Whitebox URL bij de Hapbox. Het betreffende veld is toegevoegd aan de search-and-copy call, die nu de volgende signature heeft:
Request:
```
POST /haplink/topicus/0.2/search-and-copy
{"bsn": "123443210"}
```
Response:
```
200 Ok
{
"url": "https://vrbld-amsterdam.mcs-net.nl/a6516we98r4w9e8/HL7v3-PS",
"challenge": "4257+pGn6EDeoiHYGKckFQ"
}
```
De challenge wordt samen met de BSN, URL en UZI pas gegevens ondertekend volgens de ([JWS standaard](https://datatracker.ietf.org/doc/html/rfc7515)).
Het proces is als volgt:
1) Initialiseer een WBXAuthToken met de URL, BSN van de patient, challenge en UZI certificaat van de huidige sessie
2) Roep de getHashBag methode aan
3) Roep de ITransactionManager.sign (ZorgID SDK) methode aan met de waarde die de getHashBag methode terug geeft
3) Wacht op de callback van de transaction manager (ZorgID SDK)
4) Extraheer de ondertekende data (eventArgs.signedDataBag.content[0].data)
5) Roep de convertAndSetSignature methode aan met de ondertekende data
Nu is het WBXAuthToken gereed om een dossier bij de Whitebox op te halen.
Om het dossier op te halen moet er een HTTP POST request wordt gedaan vanuit een iFrame met de volgende velden:
```
url=[de Whitebox url]
x-access-token=[de jws]
```
Voorbeeld van de content die in het iFrame moet worden geladen:
```
<html>
<head></head>
<body>
<form method='POST' action='${wbxAuthToken.endpoint()}'>
<input type='hidden' name='x-access-token' value='${wbxAuthToken.jws()}'>
<input type='hidden' name='url' value='${wbxAuthToken.url()}'>
</form>
<script>document.querySelector('form').submit()</script>
</body>
</html>
```
Er zijn verschillende manieren denkbaar om bovenstaande HTML in een iframe te krijgen:
1) genereer een data url van de inhoud van het iframe en geeft deze mee als het *src* attribuut
2) geef de inhoud van de iFrame mee het *srcdoc* attribuut
3) render de inhoud van het iFrame op een specifiek endpoint binnen Topicus HAP en geeft dit op als het *src* attribuut van het iFrame
Methode 1 en 2 werken alleen als de CSP van de pagina waarop de iFrame wordt geladen dit toelaat.
De pagina die wordt geladen na het POST verzoek zal, net als in de huidige implementatie, een message posten op zijn parent window met daarin de hoogte van de content zodat de parent window daarop het iFrame kan resizen.
Voorbeeld message
```
{
'height': 500
}
```
## DVPA-r proces
Naast token authenticatie via een iFrame heeft Whitebox Systems ook de DVPA-r (dienstverlener push autorisatie - receiver) interface ontwikkeld. Deze interface werk met een compatible token. Ook dit token kan dus gegenereerd worden met deze bibliotheek. De benodigde informatie voor het instantiëren van een Whitebox authenticatietoken wordt beschikbaar gesteld in een token template, aan te vragen via de DVPA-r interface. De details hierover vindt u in de [DVPA-r documentatie](https://docs.mcsr.nl/dvpa-r). De bibliotheek bevat ook een data class om een JSON representatie van de DVPA-r token template in te lezen om daar vervolgens de benodigde data uit te extraheren. Een voorbeeld hiervan vindt u in de meegeleverde voorbeeld applicatie.

Binary file not shown.

@ -0,0 +1,39 @@
{
"toComplement": [
"#/data/header/x5c",
"#/data/header/alg",
"#/data/claims/timestamp",
"#/data/claims/authorizationSubject",
"#/data/claims/user/uzi_id",
"#/data/claims/user/uzi_ura"
],
"tokenType": "JWT",
"tokenSubtype": "WhiteboxJWT",
"data": {
"allowedAlg": [
"PS512",
"PS256",
"PS384",
"RS512",
"RS384",
"RS256"
],
"header": {
"alg": "",
"x5c": ""
},
"claims": {
"url": "https://whitebox-2.blackbird-kloosterman.test.mcs-net.nl:8585/4814fd6ae69d4fc350529ed5ddd691b0/copied::mandate_permitted=yes",
"challenge": "U1WYDC_OQeUkpZQ7Ye0S4I6j4fs",
"timestamp": 0,
"authorizationSubject": "",
"user": {
"uzi_id": "",
"uzi_ura": ""
},
"system": {
"key_fp": "sha256:36ee0a339bcf01c39eac5a60ca3a06100b023bbf9ec99b7f3b12b768dc5ec0da"
}
}
}
}

@ -0,0 +1 @@
kotlin.code.style=official

Binary file not shown.

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Binary file not shown.

Binary file not shown.

@ -0,0 +1,3 @@
rootProject.name = "WbxTokenSigner"

@ -0,0 +1,46 @@
import aet.com.google.gson.annotations.SerializedName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
data class System @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("key_fp" ) var keyFp : String? = null
)
@Serializable
data class User @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("uzi_id" ) var uziId : String? = null,
@JsonNames("uzi_ura" ) var uziUra : String? = null
)
@Serializable
data class Claims @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("url" ) var url : String? = null,
@JsonNames("challenge" ) var challenge : String? = null,
@JsonNames("timestamp" ) var timestamp : Int? = null,
@JsonNames("authorizationSubject" ) var authorizationSubject : String? = null,
@JsonNames("user" ) var user : User? = User(),
@JsonNames("system" ) var system : System? = System()
)
@Serializable
data class Header @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("alg" ) var alg : String? = null,
@JsonNames("x5c" ) var x5c : String? = null
)
@Serializable
data class Data @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("allowedAlg" ) var allowedAlg : ArrayList<String> = arrayListOf(),
@JsonNames("header" ) var header : Header? = Header(),
@JsonNames("claims" ) var claims : Claims? = Claims()
)
@Serializable
data class DVPARTokenTemplate @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("toComplement" ) var toComplement : ArrayList<String> = arrayListOf(),
@JsonNames("tokenType" ) var tokenType : String? = null,
@JsonNames("tokenSubtype" ) var tokenSubtype : String? = null,
@JsonNames("data" ) var data : Data? = Data()
)

@ -0,0 +1,224 @@
import kotlinx.serialization.json.Json
import nl.aet.cid.client.sdk.*
import nl.aet.cid.client.sdk.desktop.Configuration
import nl.aet.cid.client.sdk.desktop.InstanceManager
import nl.aet.cid.client.sdk.types.*
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.coroutines.suspendCoroutine
import nl.whiteboxsystems.tokens.WBXAuthToken
import java.lang.System
import kotlin.system.exitProcess
fun setupManager() : IInstanceManager {
val logPath = "${File("").absolutePath}/logs/java_sdk.log"
val logTimingPath = "${File("").absolutePath}/logs/java_sdk_timing.log"
val sdkLoggerConf = LoggerConfiguration(LogLevel.Verbose, logPath, LogTarget.File)
val timingLoggerConf = LoggerConfiguration(LogLevel.Verbose, logTimingPath, LogTarget.File)
InstanceManager.initialize(Configuration("", sdkLoggerConf, timingLoggerConf))
return InstanceManager.instance()
}
class Callback(success: (args: TransactionFinishedEventArgs) -> Unit, failure: (args: TransactionCancelledEventArgs) -> Unit) {
val success = success
val failure = failure
}
class App() {
private val manager = setupManager()
private var sessionId : UUID? = null
private val transactions = mutableMapOf<UUID, Callback>()
private var elemCallback : (() -> Unit)? = null
private var elemFailedCallback : (() -> Unit)? = null
private var sessionOpened : (() -> Unit)? = null
private var sessionOpenedFailed : (() -> Unit)? = null
private var secureElement : SecureElement? = null
init {
manager.secureElementStore.addHandler(
ISecureElementStore.SECURE_ELEMENT_RETRIEVED,
SecureElementRetrievedEventHandler { _, eventArgs ->
secureElement = eventArgs.secureElement
elemCallback?.invoke()
})
manager.secureElementStore.addHandler(
ISecureElementStore.SECURE_ELEMENT_CANCELLED,
SecureElementCancelledEventHandler { _, _ ->
elemFailedCallback?.invoke()
})
manager.sessionManager.addHandler(ISessionManager.SESSION_OPENED,
SessionOpenedEventHandler { _, _ ->
sessionOpened?.invoke()
})
manager.sessionManager.addHandler(ISessionManager.SESSION_OPEN_CANCELLED,
SessionOpenCancelledEventHandler { _, _ ->
sessionOpenedFailed?.invoke()
})
manager.transactionManager.addHandler(ITransactionManager.TRANSACTION_FINISHED,
TransactionFinishedEventHandler { _, eventArgs ->
val cb = transactions[eventArgs.transactionId]
cb?.success?.invoke(eventArgs)
})
manager.transactionManager.addHandler(ITransactionManager.TRANSACTION_CANCELLED,
TransactionCancelledEventHandler { _, eventArgs ->
val cb = transactions[eventArgs.transactionId]
cb?.failure?.invoke(eventArgs)
})
}
private suspend fun retSecElem() {
manager.secureElementStore.retrieve()
return suspendCoroutine { cont ->
this.elemCallback ={
cont.resumeWith(Result.success(Unit))
}
this.elemFailedCallback ={
cont.resumeWith(Result.failure(IOException("Could not retrieve Secure Element")))
}
}
}
private suspend fun openSession(pin: String) : UUID {
val sessionId: UUID = manager.sessionManager.openAttached(pin.toCharArray())
println("-- Opening session with sessionId: $sessionId")
manager.secureElementStore.retrieve()
return suspendCoroutine { cont ->
this.sessionOpened ={
cont.resumeWith(Result.success(sessionId))
}
this.sessionOpenedFailed ={
cont.resumeWith(Result.failure(IOException("Could not open session")))
}
}
}
suspend fun init(pin: String) {
retSecElem()
val sessionId = openSession(pin)
this.sessionId = sessionId
println("-- Session Opened: $sessionId")
}
suspend fun sign(nonce: String, bsn: String, url: String, key_fp: String?) : WBXAuthToken {
println("---- Signing data ----")
val certData = secureElement?.certificates?.filter { x -> x.keyUsage == KeyUsage.ClientAuthentication }?.get(0)
val token = WBXAuthToken(nonce, bsn, url, certData, key_fp)
val hashBag = token.getHashBag()
val hashTransactionId = manager.transactionManager.sign(sessionId, hashBag)
return suspendCoroutine { cont ->
val callback = Callback({
val result = it.signedDataBag.content.values.iterator().next().data
token.convertAndSetSignature(result)
cont.resumeWith(Result.success(token))
}, {
cont.resumeWith(Result.failure(IOException("Could not open session")))
})
this.transactions[hashTransactionId] = callback
}
}
fun shutdown() {
manager.dispose()
}
}
fun renderHTML(wbxAuthToken : WBXAuthToken) : String {
val startURL = wbxAuthToken.endpoint()
val data = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Topicus HAP mock</h1>
<iframe
style="width: 100%"
srcdoc="<html>
<head></head>
<body>
<form method='POST' action='${startURL}'>
<input type='hidden' name='x-access-token' value='${wbxAuthToken.jws()}'><input type='hidden' name='url' value='${wbxAuthToken.url()}'>
</form>
<script>document.querySelector('form').submit()</script>
</body>
</html>
" frameborder="0">
</iframe>
<script>
var iframe = document.querySelector('iframe')
window.addEventListener('message', function (e) {
iframe.style.height = e.data.height + 'px'
});
</script>
</body>
</html>"""
return Base64.getEncoder().encodeToString(data.toByteArray())
}
suspend fun main(args: Array<String>) {
val instance = App()
val scanner = Scanner(System.`in`)
println("---- Setup ----")
println("-- Make sure the card is correctly inserted in the terminal, enter pin and press enter.")
val pincode = scanner.nextLine()
instance.init(pincode)
println("-- Use dvpa-r template? y/n.")
val useDvpar = scanner.nextLine() == "y"
var nonce = ""
var url = ""
var key_fp : String? = null
if (useDvpar) {
val fileContent = File("./dvpa-r-data/token_template.json").readText()
val tokenTemplate = Json.decodeFromString(DVPARTokenTemplate.serializer(), fileContent)
nonce = tokenTemplate.data?.claims?.challenge.toString()
url = tokenTemplate.data?.claims?.url.toString()
key_fp = tokenTemplate.data?.claims?.system?.keyFp.toString()
} else {
println("-- Enter challenge")
nonce = scanner.nextLine()
println("-- Enter url")
url = scanner.nextLine()
}
println("-- Enter patientBsn")
var bsn = scanner.nextLine()
val wbxToken = instance.sign(nonce, bsn, url, key_fp)
if (useDvpar) {
File("./dvpa-r-data/token.json").writeText(wbxToken.jws())
println("Written to ./dvpa-r-data/token.json")
} else {
val data = renderHTML(wbxToken)
Runtime.getRuntime().exec("C:\\Program\\ Files\\Google\\Chrome\\Application\\chrome.exe data:text/html;base64,${data}")
}
instance.shutdown()
exitProcess(0)
}

@ -0,0 +1,171 @@
package nl.whiteboxsystems.tokens
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import nl.aet.cid.client.sdk.SignDataRequest
import nl.aet.cid.client.sdk.SignDataRequestBag
import nl.aet.cid.client.sdk.types.Certificate
import nl.aet.cid.client.sdk.zorgid.constants.HashAlgorithNames
import nl.aet.cid.client.sdk.zorgid.constants.ProfileNames
import java.io.ByteArrayInputStream
import java.net.URL
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.util.*
val PKCS1V15Sha256Prefix = byteArrayOfInts(0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20)
object KDateTime : KSerializer<Calendar?> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Calendar?) {
if (value != null) {
encoder.encodeLong(value.timeInMillis / 1000)
}
}
override fun deserialize(decoder: Decoder): Calendar? {
val timestamp = decoder.decodeLong()
val c = Calendar.getInstance()
c.timeInMillis = timestamp * 1000
return c
}
}
@Serializable
data class User(val uzi_id: String, val uzi_ura: String) {}
@Serializable
data class Header(val x5c: List<String>, val alg : String = "") {
fun Json() : String {
return Json.encodeToString(this)
}
fun Compact() : String {
return Base64.getUrlEncoder().withoutPadding().encodeToString(Json().toByteArray())
}
}
@Serializable
data class System(val key_fp: String) {}
@Serializable
data class Payload(val challenge: String, val authorizationSubject: String, val url : String, val user: User, val system: System) {
@Serializable(with = KDateTime::class)
var timestamp : Calendar? = null
fun Json() : String {
return Json.encodeToString(this)
}
fun Compact() : String {
return Base64.getUrlEncoder().withoutPadding().encodeToString(Json().toByteArray())
}
}
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
fun extractCertFP(cert: Certificate): String {
val message: ByteArray = Base64.getDecoder().decode(cert.rawValue)
val certFactory: CertificateFactory = CertificateFactory.getInstance("X509")
val inputStream = ByteArrayInputStream(message)
val _cert: java.security.cert.Certificate? = certFactory.generateCertificate(inputStream)
val md = MessageDigest.getInstance("SHA-256")
val digest: ByteArray = md.digest(_cert?.publicKey?.encoded)
val fp = "sha256:${digest.toHex()}"
println("fp: ${fp}")
return fp
}
fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
class WBXAuthToken(challenge: String, bsn: String, url :String, cert: Certificate?, system_key_fp: String?) {
private var header = cert?.let { Header(listOf(it.rawValue), "RS256") }
private var payload = cert?.parsedFields?.get("UziNumber")?.let { cert?.parsedFields?.get("UziRegisterSubscriberNumber")?.let { it1 -> User(it, it1) } }
?.let { Payload(challenge, bsn, url, it, system_key_fp?.let { fp -> System(fp) } ?: System(extractCertFP(cert))) }
private var signature = ""
init {
updateTimestamp()
}
private fun getSignData() : String {
return "${header?.Compact()}.${payload?.Compact()}"
}
private fun getSignDataHashed() : String {
val signData = getSignData()
val digest = MessageDigest.getInstance("SHA-256")
val encodedHash = digest.digest(
signData.toByteArray()
)
// The ZorgID SDK signs without applying EMSA-PKCS1-v1_5 encoding. To accommodate
// for this, prepend the sha256 OID to the signed data so that it complies to the JWS
// RS256 algorithm.
// For more info: https://datatracker.ietf.org/doc/html/rfc3447#section-9.2
return Base64.getEncoder().encodeToString(PKCS1V15Sha256Prefix + encodedHash)
}
// updateTimestamp set the token's timestamp to now. updateTimestamp is called
// when de token is instantiated. It can be called on a later moment to update the timestamp.
// This can be useful if there is much time between the instantiation and actual signing
// of the token. Note: don't update timestamp after calling the getHashBag method.
// The timestamp is part of the signed data so calling teh update timestamp method after signing
// the data results in an invalid JWS
fun updateTimestamp() {
payload?.timestamp = Calendar.getInstance()
}
// getHashBag returns the data that needs to be signed, wrapped in a SignDataRequestBag.
// The return value of this method can be used directly to call the ITransactionManager.sign method.
fun getHashBag() : SignDataRequestBag {
val hashBag = SignDataRequestBag(ProfileNames.Hash1, HashAlgorithNames.Sha256)
hashBag.add(SignDataRequest(getSignDataHashed()))
return hashBag
}
// convertAndSetSignature converts the signature returned by the ZorgID SDK. This signature is in
// base64 std encoding. JWS's need to have base64 URL encoding without padding.
fun convertAndSetSignature(sig : String) {
signature = Base64.getUrlEncoder().withoutPadding().encodeToString(Base64.getDecoder().decode(sig))
}
// jws returns the WBXToken in jws compact format
fun jws() : String {
return this.getSignData() + "." + signature
}
// url returns the Whitebox url for which this token provides access
fun url() : String {
if (payload == null) {
return ""
}
return payload!!.url
}
// endpoint returns the endpoint to which the JWS and URL need to be posted
fun endpoint() : String {
if (payload == null) {
return ""
}
var parsed = URL(payload?.url)
var startURL = "${parsed.protocol}://${parsed.host}"
if (parsed.port > 0 && parsed.port != 443) {
startURL += ":${parsed.port}"
}
startURL += "/topicus"
return startURL
}
}

@ -0,0 +1,7 @@
Manifest-Version: 1.0
Main-Class: MainKt
Class-Path: kotlin-stdlib-1.5.31.jar cid-sdk-zorg-id.jar kotlin-stdlib-c
ommon-1.5.31.jar kotlin-stdlib-jdk7-1.5.31.jar annotations-13.0.jar kot
linx-serialization-core-jvm-1.3.0.jar kotlinx-serialization-json-jvm-1.
3.0.jar kotlin-stdlib-jdk8-1.5.31.jar cid-sdk-java.jar
Loading…
Cancel
Save