Manitoba Immunization Card Validation Application Breakdown

The Manitoba Government has built an application to validate immunization cards are valid. The application is very limited in what it is able to do.

On launching the application you will get a logon screen to sign in to your immunizationcard.manitoba.ca account, why you need to sign in only to validate an immunization card is odd as there is no limitations on the usage of the application. To create an account all you need is a Manitoba Health Care Card.

Once signed in the application it will ask for access to the camera and offer a scanning interface. This is interface does not automatically scan any cards you must press a button when the QRcode is centered in the box on screen. Then the application will show the name and immunization stats of the card owner. Now to be clear this does not show any details other then a pass fail, you will not see when or what vaccines they have received.

Now being a simple application I was surprised that this APK was over 50MB in size and decided to dig in to it. After decompiling the APK i found several interesting things.

  • They are using Tenserflow models to get the grey scale of the image to locate the QR code. They actually have many Tenserflow models and that some raise questions. It might be that they simple did not clean up and left the default Tenserflow face detection models in place and are not using them.
    After digging through the code there is many references to face detection, now this might be a simple lazy coder that left all of the references in place and is not using the feature.
Tenserflow models
  • The second Item that really stands out to me is that they have implemented the Stripe payment system in this application. There should be no reason for this to exist but it is clearly setup for some payment activity.
<activity android:exported="true" android:launchMode="singleTask" android:name="expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="expo.modules.payments.stripe.6883bdb6-7f1d-4578-8de0-29529ca3d0e0"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi40_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi40_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi39_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi39_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi38_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi38_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi37_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi37_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity><activity android:exported="true" android:launchMode="singleTask" android:name="expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="expo.modules.payments.stripe.6883bdb6-7f1d-4578-8de0-29529ca3d0e0"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi40_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi40_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi39_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi39_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi38_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi38_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
<activity android:exported="true" android:launchMode="singleTask" android:name="abi37_0_0.expo.modules.payments.stripe.RedirectUriReceiver" android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
	<intent-filter>
		<action android:name="android.intent.action.VIEW"/>
		<category android:name="android.intent.category.DEFAULT"/>
		<category android:name="android.intent.category.BROWSABLE"/>
		<data android:scheme="abi37_0_0.expo.modules.payments.stripe"/>
	</intent-filter>
</activity>
  • There is a Facebook campaign tracker setup.
<receiver android:exported="true" android:name="com.facebook.CampaignTrackingReceiver" android:permission="android.permission.INSTALL_PACKAGES">
	<intent-filter>
		<action android:name="com.android.vending.INSTALL_REFERRER"/>
	</intent-filter>
</receiver>

Conclusion is that this application does not have any ill intent built in, but there is several very questionable bits of code, Tenserflow model’s, and payment systems linked in this application that should not exist if it is only for the intended usage. I expect this was an outsourced application that was created with little oversight creating a mess of copy and pasting with no code review.

Update:
I would like to share this implementation of the validation system created by markjenkins that is 104 lines and less then 4KB in size.
https://github.com/markjenkins/immunizedshellscriptmb
You can read about it here as well https://hackaday.com/2021/08/19/manitoban-makes-open-software-demo-of-proprietary-vaccine-verification-systems/

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.