Compare commits

...

91 Commits

Author SHA1 Message Date
TanelOrumaa 869f11f8a8 Merge branch 'main' of https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC 2022-01-18 01:25:59 +02:00
TanelOrumaa bfa5a91ef3
Merge pull request #19 from TanelOrumaa/MOB-55
MOB-55 to main
2022-01-18 01:16:30 +02:00
TanelOrumaa d67c815aad
Merge pull request #21 from TanelOrumaa/testappchanges
Testappchanges to MOB-55
2022-01-18 00:54:23 +02:00
TanelOrumaa c28fc2be48 Merge branch 'main' of https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC 2022-01-18 00:35:16 +02:00
TanelOrumaa c232a1f734 Fixed the error handling a bit, added some text to login page 2022-01-18 00:34:45 +02:00
Henrik Lepson b95115af4f added comment 2022-01-17 17:16:56 +02:00
Henrik Lepson 866c3c24a7 deleted unused code 2022-01-17 17:15:45 +02:00
Henrik Lepson 32336ffb2b small changes to the return data to the test mobile app 2022-01-17 17:13:40 +02:00
Kevin 7edd8189a4
Update README.md 2021-12-14 22:52:08 +02:00
Henrik Lepson b889b9cda7 fixed testapp compatibility issue 2021-12-14 22:46:35 +02:00
TanelOrumaa e5931692b6 MOB-55 Fixed an issue with session invalidation. 2021-12-14 19:50:11 +02:00
TanelOrumaa b66c2386f0 MOB-55 Small addition to readme 2021-12-12 20:25:24 +02:00
TanelOrumaa 04933f2705 web-eid.js is now fetched from git, fixed a small problem related to authentication 2021-12-12 20:22:27 +02:00
TanelOrumaa 8b78ddf51a MOB-55 Fixed some issues with session management. 2021-12-12 20:06:08 +02:00
TanelOrumaa 13a0a9430f MOB-40 Changed header type from string to map and now will be used in requests 2021-12-12 20:05:18 +02:00
TanelOrumaa b565f6846d MOB-55 Demo website 2021-12-07 00:05:06 +02:00
TanelOrumaa d92656d982 MOB-55 Fixed bug with mobile app 2021-12-07 00:01:20 +02:00
TanelOrumaa 0da3e17b28 MOB-55 Fixed a bug with url 2021-12-07 00:00:51 +02:00
TanelOrumaa 2b660eeda0 Merge branch 'compatibility' into MOB-55 2021-12-06 23:39:35 +02:00
TanelOrumaa 5719712bef MOB-55 Disabled CSRF 2021-12-06 23:39:13 +02:00
Henrik Lepson 1e26f83db2 tweaked url 2021-12-06 23:27:43 +02:00
TanelOrumaa 44430bfab2 Merge branch 'compatibility' into MOB-55 2021-12-06 23:03:56 +02:00
TanelOrumaa 7482c88a4e MOB-55 Added session cookies reading 2021-12-06 23:01:10 +02:00
Henrik Lepson 74d97827f8 testing new approach 2021-12-06 22:08:04 +02:00
TanelOrumaa 4096201bef Merge branch 'main' into MOB-55 2021-12-06 21:09:07 +02:00
TanelOrumaa da2dbeb0fc MOB-42 Redid the whole frontend part in Vue 2021-12-06 21:08:15 +02:00
Henrik Lepson 60207319b7
Merge pull request #16 from TanelOrumaa/newapproach
Improved code
2021-12-06 20:27:00 +02:00
TanelOrumaa 7daea4b6c2 MOB-55 Added vue frontend 2021-12-06 18:14:01 +02:00
Henrik Lepson e5300dfa5e got rid of git status syntax 2021-12-04 17:21:07 +02:00
Henrik Lepson d4c2a11521 added more error messages 2021-12-04 17:08:58 +02:00
Henrik Lepson 09c4fa6be3 fixed small issue in testmobileapp 2021-12-04 12:46:40 +02:00
Henrik Lepson 63bc89b0e4
Merge pull request #14 from TanelOrumaa/iter4UI
UI/UX improvements for iteration 4
2021-12-03 16:24:00 +02:00
Henrik Lepson 152fd16162 MOB-40 finished home fragment 2021-12-03 16:11:48 +02:00
Henrik Lepson 716b983389 MOB-40 made user fragment scrollable, changed app logo 2021-11-29 20:22:20 +02:00
Henrik Lepson 94fad95364 MOB-40 added listening to NFC adapter changes 2021-11-28 16:54:26 +02:00
Henrik Lepson c33fba1a14 MOB-40 pin toggle status saved, added informational snackbars 2021-11-28 16:24:08 +02:00
Henrik Lepson 825335ea5f MOB-40 added save can to home fragment, fixed language resources 2021-11-27 21:16:50 +02:00
Henrik Lepson 0f6f31c995 MOB-40 improved authentication UX 2021-11-25 18:09:45 +02:00
Henrik Lepson 762a8c8cc2 MOB-40 changed the settings view and fixed menu notifications 2021-11-25 16:13:35 +02:00
Henrik Lepson 1138abcb11 MOB-40 home fragment logic changed 2021-11-25 14:17:00 +02:00
Henrik Lepson f085076631 MOB-40 improved CAN and PIN views 2021-11-24 20:44:40 +02:00
Henrik Lepson edc444c027 MOB-40 added xml res files for styles and dimensions 2021-11-24 18:24:21 +02:00
Henrik Lepson df5febabb7 deleted unused code 2021-11-24 18:20:22 +02:00
Henrik Lepson 1b9a59d4eb
Removed irrelevant information from the readme
The main readme contained information that was not up-to-date. Mobile Auth App capabilities clarified as well.
2021-11-23 11:40:07 +02:00
TanelOrumaa bbd5039a0b
Merge pull request #11 from TanelOrumaa/MOB-42
Backend + frontend + MOB-21 JWT creation.
2021-11-17 09:58:06 +02:00
Henrik Lepson 2c5430977d
Updated main readme 2021-11-17 09:31:30 +02:00
Henrik Lepson 68a7db2e77
Created a readme for TestMobileApp 2021-11-17 09:26:21 +02:00
Henrik Lepson a4caf24a35 MOB-41 fixed some remaining issues 2021-11-17 09:15:29 +02:00
TanelOrumaa 5b70a8f997 MOB-42 Added log out button to backend, fixed issue with challenge for test app 2021-11-16 21:30:58 +02:00
Henrik Lepson 168c9be010 fixed app not closing bug, when started from website 2021-11-14 10:13:40 +02:00
TanelOrumaa 636beeb7f3 MOB-42 Fixed token authentication issues (wrong library version, cache getting recreated every request, origin in wrong form) 2021-11-11 21:47:27 +02:00
TanelOrumaa 9b0cb1a22d MOB-42 Changed POST request logic 2021-11-08 23:52:38 +02:00
TanelOrumaa 1d665f02bf MOB-42 Added JWT emitting 2021-11-08 23:17:36 +02:00
TanelOrumaa 82b1538867 MOB-42 Fixed JWT generation issue. 2021-11-08 22:41:09 +02:00
Henrik Lepson c51f0e4277
Release notes updated 2021-11-08 21:51:54 +02:00
Henrik Lepson aaa8d8f13c
Merge pull request #12 from TanelOrumaa/fixesIter3
Iter3 related fixes
2021-11-08 20:36:12 +02:00
Henrik Lepson d60cecdd26 forgot to remove dummy values 2021-11-08 20:26:17 +02:00
Henrik Lepson f0c7ab96bb MOB-23 MOB-41 small fixes related to HTTP 2021-11-08 20:22:33 +02:00
Henrik Lepson 1b79eba4a4 MOB-23 setup for post request done, parameter viewModel added 2021-11-08 19:07:30 +02:00
TanelOrumaa c46c3082b7 Merged main to branch to support DeepLink 2021-11-08 18:02:56 +02:00
TanelOrumaa e4a9a4da1b Added readme to backend demo project 2021-11-08 17:49:42 +02:00
TanelOrumaa f9cd30922e MOB-42 Added backend server, two frontend webpages and rest endpoints for getting challenge, submitting authentication token and getting authentication object. MOB-21 Added JWT creation, but whole process still needs some work. 2021-11-08 17:30:56 +02:00
Henrik Lepson 4252e3e637
Merge pull request #10 from TanelOrumaa/authprep
Authprep
2021-11-07 14:35:52 +02:00
Henrik Lepson 64357ca1d3 MOB-23 token emit related code 2021-11-07 14:14:22 +02:00
Henrik Lepson eca3f92468 MOB-41 small changes to the test app 2021-11-07 13:49:19 +02:00
TanelOrumaa 44469b8533 Merge remote-tracking branch 'origin/authJWT' into MOB-42 2021-11-07 13:30:09 +02:00
Henrik Lepson cc3a3c10d6 MOB-42 app can be launched with deep links 2021-11-05 20:21:22 +02:00
Henrik Lepson 08430c897c MOB-37 added english translation to the app 2021-10-30 17:07:17 +03:00
Henrik Lepson ebc541af08 MOB-13 pin2 fragment created 2021-10-30 16:51:38 +03:00
Henrik Lepson a9336b790f MOB-41 created a base for app that can launch authapp 2021-10-26 11:42:58 +03:00
Henrik Lepson 364fc7c45b MOB-40 started to create UI for auth when launched with intent 2021-10-21 21:22:11 +03:00
Henrik Lepson 71db5cc9e6
Merge pull request #9 from TanelOrumaa/iter3UI
Iter3 UI
2021-10-21 20:31:26 +03:00
Lemmo Lavonen 62888a7299 Add a method for signing the auth token hash. 2021-10-19 00:58:53 +03:00
Henrik Lepson 3e5f02f842 made some changes related to the demo next week 2021-10-16 14:02:15 +03:00
Henrik Lepson bd686739fc MOB-40 changed nav logic, refactored Home, Pin and Can fragments 2021-10-16 13:20:55 +03:00
Henrik Lepson 2678fd3c42 MOB-38 MOB-40 PIN1 save functionality added, pin/can nav order changed 2021-10-16 11:02:48 +03:00
Henrik Lepson 1dd11b21fc MOB-38 added PIN1 UI components to settings menu 2021-10-16 09:47:57 +03:00
Henrik Lepson 8637a4182a MOB-39 can can be now stored on the phone 2021-10-15 22:12:06 +03:00
Henrik Lepson 141dfb18db MOB-38 MOB-39 MOB-40 created UI and changed navigation for CAN saving 2021-10-15 18:23:21 +03:00
Henrik Lepson 48817f9057 MOB-38 created basic menu layout, cur no actions 2021-10-15 17:06:00 +03:00
Lemmo Lavonen 850ab8fc66 Use hex to represent bytes. 2021-10-14 19:50:53 +03:00
Lemmo Lavonen 824d33d635 Merge branch 'main' into authJWT 2021-10-14 04:01:47 +03:00
Lemmo Lavonen ef7015abb8 Refactor (generalise selecting and reading a file, PIN verification and certificate retrieval). 2021-10-14 03:58:49 +03:00
Lemmo Lavonen 29c7ecfa12 Refactor the method for authentication certificate retrieval. 2021-10-13 02:27:19 +03:00
Lemmo Lavonen 9c48cc9c1a Fix authentication certificate retrieval. 2021-10-12 12:18:06 +03:00
Lemmo Lavonen 25c01803cb auth cert bug fix progress 2021-10-12 03:04:00 +03:00
Lemmo Lavonen 1c8a606376 Add a method for getting the authentication certificate (WIP). 2021-10-12 00:36:08 +03:00
Lemmo Lavonen d2ad8920a1 Add a method for verifying PIN1. 2021-10-12 00:34:06 +03:00
TanelOrumaa 96595d924b
Added last updated times for wiki pages. 2021-10-10 23:52:59 +03:00
TanelOrumaa 4ae5d3c0c4
Changed formatting for Readme 2021-10-10 23:40:36 +03:00
TanelOrumaa 490a04bb44
Update Readme with information on how to run the app 2021-10-10 23:39:37 +03:00
135 changed files with 33224 additions and 482 deletions

View File

@ -59,4 +59,13 @@ dependencies {
//For cryptography
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69'
//SecureDataStoring
implementation("androidx.security:security-crypto:1.0.0")
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
'org.bouncycastle:bcprov-jdk15on:1.60',
'io.jsonwebtoken:jjwt-gson:0.11.2'
implementation 'com.koushikdutta.ion:ion:3.1.0'
}

View File

@ -3,6 +3,7 @@
package="com.tarkvaraprojekt.mobileauthapp">
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
@ -19,6 +20,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- for launching the app with deep links -->
<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="authapp" android:host="start" android:path="/" />
</intent-filter>
</activity>
</application>

View File

@ -2,7 +2,9 @@ package com.tarkvaraprojekt.mobileauthapp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.TagLostException
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.os.CountDownTimer
@ -10,14 +12,18 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import java.lang.Exception
import kotlin.concurrent.thread
import kotlin.system.exitProcess
/**
* Fragment that asks the user to detect the ID card with mobile NFC chip.
@ -28,7 +34,12 @@ class AuthFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentAuthBinding? = null
private val paramsModel: ParametersViewModel by activityViewModels()
private var _binding: FragmentAuthBinding? = null
private val binding get() = _binding!!
private val args: CanFragmentArgs by navArgs()
private lateinit var timer: CountDownTimer
@ -39,8 +50,8 @@ class AuthFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentAuthBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentAuthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -49,49 +60,92 @@ class AuthFragment : Fragment() {
override fun onTick(p0: Long) {
timeRemaining--
if (timeRemaining == 0) {
binding!!.timeCounter.text = getString(R.string.no_time)
binding.timeCounter.text = getString(R.string.no_time)
} else {
binding!!.timeCounter.text = getString(R.string.time_left, timeRemaining)
binding.timeCounter.text = getString(R.string.time_left, timeRemaining)
}
}
override fun onFinish() {
Thread.sleep(750)
goToTheStart()
cancelAuth(408)
}
}.start()
binding!!.nextButton.setOnClickListener { goToNextFragment() }
binding!!.cancelButton.setOnClickListener { goToTheStart() }
// The button exists in code for testing reasons, but not visible to the user anymore unless visibility is changed in the code.
binding.nextButton.visibility = View.GONE
binding.nextButton.setOnClickListener { goToNextFragment() }
binding.cancelButton.setOnClickListener { cancelAuth(444) }
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter != null)
getInfoFromIdCard(adapter)
else { // If NFC adapter can not be detected then end the auth process as it is not possible to read an ID card
cancelAuth(447) // It would be a good idea to show user some notification as it might be confusing if the app suddenly closes
}
}
private fun goToNextFragment() {
timer.cancel()
val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile)
findNavController().navigate(action)
}
private fun cancelAuth(code: Int) {
viewModel.clearUserInfo()
timer.cancel()
if (args.mobile) {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
(activity as MainActivity).returnError(code)
requireActivity().finishAndRemoveTask()
}
}
private fun getInfoFromIdCard(adapter: NfcAdapter) {
adapter.enableReaderMode(activity, { tag ->
timer.cancel()
requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.card_detected)
binding.timeCounter.text = getString(R.string.card_detected)
}
val card = IsoDep.get(tag)
card.timeout = 32768
card.use {
try {
val comms = Comms(it, viewModel.userCan)
val response = comms.readPersonalData(byteArrayOf(1, 2, 6))
viewModel.setUserFirstName(response[1])
viewModel.setUserLastName(response[0])
viewModel.setUserIdentificationNumber(response[2])
requireActivity().runOnUiThread{
binding!!.timeCounter.text = getString(R.string.data_read)
val jws = Authenticator(comms).authenticate(
paramsModel.challenge,
paramsModel.origin,
viewModel.userPin
)
paramsModel.setToken(jws)
requireActivity().runOnUiThread {
goToNextFragment()
}
} catch (e: Exception) {
requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.no_success)
when(e) {
is TagLostException -> requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.id_card_removed_early)
cancelAuth(444)
}
else -> {
when ("invalid pin") {
in e.message.toString().lowercase() -> requireActivity().runOnUiThread {
val messagePieces = e.message.toString().split(" ")
binding.timeCounter.text = getString(R.string.wrong_pin, messagePieces[messagePieces.size - 1])
viewModel.deletePin(requireContext())
cancelAuth(449)
}
else -> requireActivity().runOnUiThread {
binding.timeCounter.text = getString(R.string.wrong_can_text)
viewModel.deleteCan(requireContext())
cancelAuth(449)
}
}
}
}
// Gives user some time to read the error message
Thread.sleep(1000)
goToTheStart()
// Give user some time to read the error message
Thread.sleep(2000)
} finally {
adapter.disableReaderMode(activity)
}
@ -99,19 +153,8 @@ class AuthFragment : Fragment() {
}, NfcAdapter.FLAG_READER_NFC_A, null)
}
private fun goToNextFragment() {
timer.cancel()
findNavController().navigate(R.id.action_authFragment_to_userFragment)
}
private fun goToTheStart() {
viewModel.clearUserInfo()
timer.cancel()
findNavController().navigate(R.id.action_authFragment_to_homeFragment)
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@ -1,61 +1,134 @@
package com.tarkvaraprojekt.mobileauthapp
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import org.w3c.dom.Text
/**
* Fragment that deals with asking the user for six digit CAN
* Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved
* then the fragment is skipped immediately and if the CAN is not saved then the user
* is asked whether it should be saved for the future or not before continuing.
*/
class CanFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentCanBinding? = null
private var _binding: FragmentCanBinding? = null
private val binding get() = _binding!!
// Navigation arguments:
// saving = true means that we are navigating here from the settings menu and must return to the settings menu.
private val args: CanFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentCanBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentCanBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.nextButton.setOnClickListener { goToNextFragment() }
binding!!.cancelButton.setOnClickListener { goToTheStart() }
checkIfSkip()
binding.canTextField.editText?.addTextChangedListener {
checkEnteredCan()
}
binding.buttonCancel.setOnClickListener { goToTheStart() }
}
private fun goToNextFragment() {
val enteredCan = binding!!.canEditText.editText?.text.toString()
if (enteredCan.length != 6) {
Toast.makeText(requireContext(), getString(R.string.length_can), Toast.LENGTH_SHORT)
.show()
} else {
viewModel.setUserCan(
binding!!.canEditText.editText?.text.toString()
)
findNavController().navigate(R.id.action_canFragment_to_authFragment)
/**
* Checks if the current fragment can be skipped or not.
* If the user has CAN saved on the device there is no need to ask it again.
*/
private fun checkIfSkip() {
if (viewModel.userCan.length == 6) {
goToTheNextFragment()
}
}
/**
* Takes user to the next fragment, which is PinFragment.
*/
private fun goToTheNextFragment() {
val action = CanFragmentDirections.actionCanFragmentToPinFragment(auth = args.auth, mobile = args.mobile)
findNavController().navigate(action)
}
/**
* Navigates the user back to the start depending on where the user arrived.
* If the user arrived from the settings menu then the start is the settings menu
* not the HomeFragment.
*/
private fun goToTheStart() {
viewModel.clearUserInfo()
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
if (args.saving) {
if (args.fromhome) {
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
} else {
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
}
} else if (args.auth || args.mobile) {
if (args.mobile) {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
(activity as MainActivity).returnError(444)
requireActivity().finishAndRemoveTask()
}
} else {
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
}
}
/**
* Method that creates and shows a snackbar that tells the user that CAN has been saved
*/
private fun showSnackbar() {
val snackbar = Snackbar.make(requireView(), R.string.can_status_saved, Snackbar.LENGTH_SHORT)
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
snackbar.show()
}
/**
* Checks whether the user has entered a 6 digit can to the input field.
* If yes then the user is allowed to continue otherwise the user is
* allowed to modify the entered can.
*/
private fun checkEnteredCan() {
val enteredCan = binding.canTextField.editText?.text.toString()
if (enteredCan.length == 6) {
viewModel.setUserCan(enteredCan)
viewModel.storeCan(requireContext()) //Maybe storeCan should always automatically call setUserCan method as well because these methods usually are used together
showSnackbar()
if (args.saving) {
goToTheStart()
} else {
goToTheNextFragment()
}
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@ -1,42 +1,371 @@
package com.tarkvaraprojekt.mobileauthapp
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.nfc.TagLostException
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.koushikdutta.ion.Ion
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import org.json.JSONObject
import java.lang.Exception
import java.lang.RuntimeException
import java.net.URL
/**
* HomeFragment is only shown to the user when then the user launches the application. When the application
* is launched by another application or a website then this Fragment will be skipped.
* This fragment uses the fields from the MainActivity by casting the activity to MainActivity.
* This might not be the best practice, but the application uses a single activity design so it should
* always work.
*/
class HomeFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentHomeBinding? = null
private val intentParams: ParametersViewModel by activityViewModels()
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
// The ID card reader mode is enabled on the home fragment when can is saved.
private var canSaved: Boolean = false
// Is the app used for authentication
private var auth: Boolean = false
private var receiver: BroadcastReceiver? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentHomeBinding.inflate(inflater, container, false)
// Making settings menu active again
(activity as MainActivity).menuAvailable = true
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.beginButton.setOnClickListener { goToNextFragment() }
initialChecks()
if (requireActivity().intent.data?.getQueryParameter("action") != null) {
// Currently we only support authentication not signing.
auth = true
}
val mobile = requireActivity().intent.getBooleanExtra("mobile", false)
if (auth || mobile) {
startAuthentication(mobile)
} else {
receiver = object : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
updateAction(canSaved)
}
}
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
requireActivity().registerReceiver(receiver, filter)
updateAction(canSaved)
}
}
private fun goToNextFragment() {
findNavController().navigate(R.id.action_homeFragment_to_pinFragment)
/**
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
*/
private fun goToTheNextFragment(mobile: Boolean = false) {
(activity as MainActivity).menuAvailable = false
val action =
HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
findNavController().navigate(action)
}
/**
* Method that starts the authentication use case.
*
* NOTE: Comment out try-catch block when testing without backend
*/
private fun startAuthentication(mobile: Boolean) {
try {
if (mobile) {
// We use !! to get extras because we want an exception to be thrown when something is missing.
//intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
val challengeUrl = requireActivity().intent.getStringExtra("challenge")!!
val headers = requireActivity().intent.getStringExtra("headers")!!
val map: HashMap<String, String> = HashMap()
map.put("sessionId", headers)
intentParams.setHeaders(map)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
Ion.with(activity)
.load(challengeUrl)
.setHeader("sessionId", headers)
.asJsonObject()
.setCallback { _, result ->
try {
val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
intentParams.setChallenge(challenge)
goToTheNextFragment(mobile)
} catch (e: Exception) {
Log.i("GETrequest", e.toString())
}
}
} else { //Website
/*
var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!!
// TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back.
challenge = challenge.replace(" ", "+")
intentParams.setChallenge(challenge)
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!)
*/
var getAuthChallengeUrl =
requireActivity().intent.data!!.getQueryParameter("getAuthChallengeUrl")!!
getAuthChallengeUrl =
getAuthChallengeUrl.substring(1, getAuthChallengeUrl.length - 1)
var postAuthTokenUrl =
requireActivity().intent.data!!.getQueryParameter("postAuthTokenUrl")!!
postAuthTokenUrl = postAuthTokenUrl.substring(1, postAuthTokenUrl.length - 1)
val headers =
getHeaders(requireActivity().intent.data!!.getQueryParameter("headers")!!)
intentParams.setAuthUrl(postAuthTokenUrl)
val address = "https://" + URL(getAuthChallengeUrl).host
intentParams.setOrigin(address)
intentParams.setHeaders(headers)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
val ion = Ion.with(activity)
.load(getAuthChallengeUrl)
// Set headers.
for ((header, value) in intentParams.headers) {
ion.setHeader(header, value)
}
ion
.asJsonObject()
.setCallback { _, result ->
try {
// Get data from the result and call launchAuth method
val challenge =
result.asJsonObject["nonce"].toString().replace("\"", "")
intentParams.setChallenge(challenge)
goToTheNextFragment(mobile)
} catch (e: Exception) {
Log.i("GETrequest", "was unsuccessful" + e.message)
throw RuntimeException()
}
}
}
} catch (e: Exception) {
// There was a problem with parameters, which means that authentication is not possible.
// In that case we will cancel the authentication immediately as it would be waste of the user's time to carry on
// before getting an inevitable error.
val message = MaterialAlertDialogBuilder(requireContext())
message.setTitle(getString(R.string.problem_parameters))
if (intentParams.challenge == "") {
message.setMessage(getString(R.string.problem_challenge))
} else if (intentParams.authUrl == "") {
message.setMessage(getString(R.string.problem_authurl))
} else if (intentParams.origin == "") {
message.setMessage(getString(R.string.problem_originurl))
} else {
message.setMessage(getString(R.string.problem_other))
}
message.setPositiveButton(getString(R.string.continue_button)) { _, _ ->
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
message.show()
}
}
/**
* Checks the state of the CAN, saved or not saved. Updates the text and logo.
*/
private fun canState() {
if (viewModel.userCan.length == 6) {
binding.canStatusText.text = getString(R.string.can_status_saved)
binding.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
canSaved = true
} else {
binding.canStatusText.text = getString(R.string.can_status_negative)
binding.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
canSaved = false
}
}
/**
* Checks the state of the PIN 1, saved or not saved. Updates the text and logo.
*/
private fun pinState() {
if (viewModel.userPin.length in 4..12) {
binding.pinStatusText.text = getString(R.string.pin_status_saved)
binding.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
} else {
binding.pinStatusText.text = getString(R.string.pin_status_negative)
binding.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
}
}
private fun getHeaders(headersString: String): Map<String, String> {
val headers = HashMap<String, String>()
val headersStringFormatted = headersString.substring(1, headersString.length - 1)
val headersJsonObject = JSONObject(headersStringFormatted)
for (name in headersJsonObject.keys()) {
headers[name] = headersJsonObject[name].toString()
}
return headers
}
/**
* Displays texts that inform the user whether the CAN and PIN 1 are saved on the device or not.
* This might help the user to save some time as checking menu is not necessary unless the user
* wishes to make changes to the saved CAN or PIN 1.
*/
private fun displayStates() {
canState()
pinState()
}
/**
* Method where all the initial checks that should be completed before any user input is accepted should be conducted.
*/
private fun initialChecks() {
viewModel.checkCan(requireContext())
viewModel.checkPin(requireContext())
displayStates()
}
/**
* Displays a help message to the user explaining what the CAN is
*/
private fun displayMessage(title: String, message: String) {
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.return_text) { _, _ -> }
.show()
val title = dialog.findViewById<TextView>(R.id.alertTitle)
title?.textSize = 24F
}
/**
* Informs user whether the ID card can be detected or not.
*/
private fun updateAction(canIsSaved: Boolean) {
if (canIsSaved) {
binding.detectionActionText.text = getString(R.string.action_detect)
enableReaderMode()
binding.homeActionButton.visibility = View.GONE
binding.homeHelpButton.visibility = View.GONE
} else {
binding.detectionActionText.text = getString(R.string.action_detect_unavailable)
binding.homeActionButton.text = getString(R.string.add_can_text)
binding.homeActionButton.setOnClickListener {
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(
saving = true,
fromhome = true
)
findNavController().navigate(action)
}
binding.homeHelpButton.setOnClickListener {
displayMessage(
getString(R.string.can_question),
getString(R.string.can_explanation)
)
}
binding.homeActionButton.visibility = View.VISIBLE
binding.homeHelpButton.visibility = View.VISIBLE
}
}
/**
* Resets the error message and allows the user to try again
*/
private fun reset() {
binding.homeActionButton.text = getString(R.string.try_again_text)
binding.homeActionButton.setOnClickListener {
updateAction(canSaved)
}
binding.homeActionButton.visibility = View.VISIBLE
}
/**
* Method that enables the NFC reader mode, which allows the app to communicate with the ID card and retrieve information.
*/
private fun enableReaderMode() {
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter == null || !adapter.isEnabled) {
binding.detectionActionText.text = getString(R.string.nfc_not_available)
} else {
adapter.enableReaderMode(activity, { tag ->
requireActivity().runOnUiThread {
binding.detectionActionText.text = getString(R.string.card_detected)
}
val card = IsoDep.get(tag)
card.timeout = 32768
card.use {
try {
val comms = Comms(it, viewModel.userCan)
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
viewModel.setUserFirstName(response[1])
viewModel.setUserLastName(response[0])
viewModel.setUserIdentificationNumber(response[2])
viewModel.setGender(response[3])
viewModel.setCitizenship(response[4])
viewModel.setExpiration(response[5])
requireActivity().runOnUiThread {
val action = HomeFragmentDirections.actionHomeFragmentToUserFragment()
findNavController().navigate(action)
}
} catch (e: Exception) {
when (e) {
is TagLostException -> requireActivity().runOnUiThread {
binding.detectionActionText.text =
getString(R.string.id_card_removed_early)
reset()
}
else -> requireActivity().runOnUiThread {
binding.detectionActionText.text =
getString(R.string.nfc_reading_error)
viewModel.deleteCan(requireContext())
canState()
reset()
}
}
} finally {
adapter.disableReaderMode(activity)
}
}
}, NfcAdapter.FLAG_READER_NFC_A, null)
}
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
if (receiver != null) {
requireActivity().unregisterReceiver(receiver)
}
_binding = null
}
}

View File

@ -1,24 +1,87 @@
package com.tarkvaraprojekt.mobileauthapp
import android.nfc.NfcAdapter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.viewModels
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.navArgs
import androidx.navigation.navArgs
import com.google.gson.JsonObject
import com.koushikdutta.ion.Ion
import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
/**
* The only activity of the application (single activity design).
*/
class MainActivity : AppCompatActivity() {
private lateinit var navigationController: NavController
private val paramsModel: ParametersViewModel by viewModels()
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
var menuAvailable: Boolean = true
var inMenu: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navigationController = navHostFragment.navController
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.menu_settings_option -> {
if (menuAvailable) {
navigationController.navigate(R.id.action_homeFragment_to_settingsFragment)
menuAvailable = false
inMenu = true
true
} else {
if (!inMenu) {
Toast.makeText(this, getString(R.string.menu_unavailable_message), Toast.LENGTH_SHORT).show()
}
false
}
}
else -> super.onOptionsItemSelected(item)
}
fun returnError(errorCode: Int) {
val json = JsonObject()
json.addProperty("auth-token", "")
json.addProperty("error", errorCode)
Ion.getDefault(this).conscryptMiddleware.enable(false)
val ion = Ion.with(this)
.load(paramsModel.authUrl)
for ((header, value) in paramsModel.headers) {
ion.setHeader(header, value)
}
ion
.setJsonObjectBody(json)
.asJsonObject()
.setCallback { _, _ ->
}
}
}

View File

@ -21,6 +21,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@ -30,47 +31,43 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Comms {
private static final byte[] master = { // select Main AID
0, -92, 4, 12, 16, -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0
};
private static final byte[] MSESetAT = { // manage security environment: set authentication template
0, 34, -63, -92, 15, -128, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -125, 1, 2, 0
};
private static final byte[] selectMaster = Hex.decode("00a4040c10a000000077010800070000fe00000100");
private static final byte[] GAGetNonce = { // general authenticate: get nonce
16, -122, 0, 0, 2, 124, 0, 0
};
private static final byte[] MSESetAT = Hex.decode("0022c1a40f800a04007f0007020204020483010200");
private static final byte[] GAMapNonceIncomplete = {
16, -122, 0, 0, 69, 124, 67, -127, 65
};
private static final byte[] GAGetNonce = Hex.decode("10860000027c0000");
private static final byte[] GAKeyAgreementIncomplete = {
16, -122, 0, 0, 69, 124, 67, -125, 65
};
private static final byte[] GAMapNonceIncomplete = Hex.decode("10860000457c438141");
private static final byte[] GAMutualAuthenticationIncomplete = {
0, -122, 0, 0, 12, 124, 10, -123, 8
};
private static final byte[] GAKeyAgreementIncomplete = Hex.decode("10860000457c438341");
private static final byte[] dataForMACIncomplete = {
127, 73, 79, 6, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -122, 65
};
private static final byte[] GAMutualAuthenticationIncomplete = Hex.decode("008600000c7c0a8508");
private static final byte[] masterSec = {
12, -92, 4, 12, 45, -121, 33, 1
};
private static final byte[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641");
private static final byte[] personal = { // select personal data DF
12, -92, 1, 12, 29, -121, 17, 1
};
private static final byte[] selectFile = Hex.decode("0ca4010c1d871101");
private static final byte[] read = { // read binary
12, -80, 0, 0, 13, -105, 1, 0
};
private static final byte[] readFile = Hex.decode("0cb000000d970100");
private IsoDep idCard;
private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101");
private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101");
private static final byte[] MSESetEnv = Hex.decode("0c2241A41d871101");
private static final byte[] Env = Hex.decode("8004FF200800840181");
private static final byte[] InternalAuthenticate = Hex.decode("0c8800001d871101");
private static final byte[] IASECCFID = {0x3f, 0x00};
private static final byte[] personalDF = {0x50, 0x00};
private static final byte[] AWP = {(byte) 0xad, (byte) 0xf1};
private static final byte[] QSCD = {(byte) 0xad, (byte) 0xf2};
private static final byte[] authCert = {0x34, 0x01};
private static final byte[] signCert = {0x34, 0x1f};
private final IsoDep idCard;
private final byte[] keyEnc;
private final byte[] keyMAC;
private byte ssc; // Send sequence counter.
@ -84,21 +81,12 @@ public class Comms {
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
idCard.connect();
this.idCard = idCard;
long start = System.currentTimeMillis();
byte[][] keys = PACE(CAN);
Log.i("Pace duration", String.valueOf(System.currentTimeMillis() - start));
byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8));
keyEnc = keys[0];
keyMAC = keys[1];
}
public byte[] getAuthenticationCertificate() {
return new byte[0];
}
/**
* Calculates the message authentication code
*
@ -151,40 +139,51 @@ public class Comms {
* @param CAN the card access number provided by the user
* @return the decrypted nonce
*/
private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3);
private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
byte[] decryptionKey = createKey(CAN, (byte) 3);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
return cipher.doFinal(encryptedNonce);
}
/**
* Communicates with the card and logs the response
*
* @param APDU The command
* @param log Information for logging
* @return The response
*/
private byte[] getResponse(byte[] APDU, String log) throws IOException {
byte[] response = idCard.transceive(APDU);
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException(String.format("%s failed.", log));
}
Log.i(log, Hex.toHexString(response));
return response;
}
/**
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
*
* @param CAN the card access number
*/
private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
// select the ECC applet on the chip
byte[] response = idCard.transceive(master);
Log.i("Select applet", Hex.toHexString(response));
// select the IAS-ECC application on the chip
getResponse(selectMaster, "Select the master application");
// initiate PACE
response = idCard.transceive(MSESetAT);
Log.i("Authentication template", Hex.toHexString(response));
getResponse(MSESetAT, "Set authentication template");
// get nonce
response = idCard.transceive(GAGetNonce);
Log.i("Get nonce", Hex.toHexString(response));
byte[] response = getResponse(GAGetNonce, "Get nonce");
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
// generate an EC keypair and exchange public keys with the chip
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
byte[] APDU = createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66);
response = idCard.transceive(APDU);
Log.i("Map nonce", Hex.toHexString(response));
response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce");
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
// calculate the new base point, use it to generate a new keypair, and exchange public keys
@ -192,35 +191,41 @@ public class Comms {
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
APDU = createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66);
response = idCard.transceive(APDU);
Log.i("Key agreement", Hex.toHexString(response));
response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement");
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
// generate the session keys and exchange MACs to verify them
sharedSecret = cardPublicKey.multiply(privateKey).normalize();
byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded();
byte[] keyEnc = createKey(encodedSecret, (byte) 1);
byte[] keyMAC = createKey(encodedSecret, (byte) 2);
APDU = createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65);
byte[] MAC = getMAC(APDU, keyMAC);
APDU = createAPDU(GAMutualAuthenticationIncomplete, MAC, 9);
response = idCard.transceive(APDU);
Log.i("Mutual authentication", Hex.toHexString(response));
byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
byte[] keyEnc = createKey(secret, (byte) 1);
byte[] keyMAC = createKey(secret, (byte) 2);
byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
// if the chip-side verification fails, crash and burn
if (response.length == 2) throw new RuntimeException("Invalid CAN.");
// otherwise verify chip's MAC and return session keys
APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65);
MAC = getMAC(APDU, keyMAC);
// verify chip's MAC and return session keys
MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
throw new RuntimeException("Could not verify chip's MAC."); // Should never happen.
throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen.
}
return new byte[][]{keyEnc, keyMAC};
}
/**
* Selects a file and reads its contents
*
* @param FID file identifier of the required file
* @param info string for logging
* @return decrypted file contents
*/
private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
selectFile(FID, info);
byte[] response = getResponse(new byte[0], readFile, "Read binary");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException(String.format("Could not read %s", info));
}
return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
}
/**
* Encrypts or decrypts the APDU data
*
@ -255,74 +260,182 @@ public class Comms {
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
macData[15] = ssc; // first block contains the ssc
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
macData[20] = -128; // elements are terminated by 0x80 and zero-padded to the next block
macData[20] = (byte) 0x80; // elements are terminated by 0x80 and zero-padded to the next block
System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
byte[] paddedData = Arrays.copyOf(data, length);
paddedData[data.length] = -128;
paddedData[data.length] = (byte) 0x80;
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
}
macData[35 + encryptedData.length] = -128;
macData[35 + encryptedData.length] = (byte) 0x80;
byte[] MAC = getMAC(macData, keyMAC);
// construct the APDU using the encrypted data and the MAC
byte[] APDU = new byte[incomplete.length + encryptedData.length + MAC.length + 3];
System.arraycopy(incomplete, 0, APDU, 0, incomplete.length);
byte[] APDU = Arrays.copyOf(incomplete, incomplete.length + encryptedData.length + MAC.length + 3);
if (encryptedData.length > 0) {
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
}
System.arraycopy(new byte[]{-114, 8}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
System.arraycopy(new byte[]{(byte) 0x8E, 0x08}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
System.arraycopy(MAC, 0, APDU, incomplete.length + encryptedData.length + 2, MAC.length);
ssc++;
return APDU;
}
/**
* Gets the contents of the personal data dedicated file
*
* @param FID the last bytes of file identifiers being requested
* @return array containing the data strings
* Selects a FILE by its identifier
*
*/
public String[] readPersonalData(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
byte[] response = getResponse(FID, selectFile, String.format("Select %s", info));
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException(String.format("Could not select %s", info));
}
}
String[] personalData = new String[FID.length];
byte[] data;
byte[] APDU;
byte[] response;
/**
* Gets the contents of the personal data dedicated file
*
* @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16)
* @return array containing the corresponding data strings
*/
public String[] readPersonalData(byte[] lastBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
String[] personalData = new String[lastBytes.length];
int stringIndex = 0;
// select the master application
selectFile(IASECCFID, "the master application");
// select the personal data dedicated file
data = new byte[]{80, 0}; // personal data DF FID
APDU = createSecureAPDU(data, personal);
response = idCard.transceive(APDU);
Log.i("Select personal data DF", Hex.toHexString(response));
selectFile(personalDF, "the personal data DF");
// select and read the first 8 elementary files in the DF
for (int i = 0; i < FID.length; i++) {
byte[] FID = Arrays.copyOf(personalDF, personalDF.length);
// select and read the personal data elementary files
for (byte index : lastBytes) {
byte index = FID[i];
if (index > 15 || index < 1) throw new RuntimeException("Invalid personal data FID.");
data[1] = index;
APDU = createSecureAPDU(data, personal);
response = idCard.transceive(APDU);
Log.i(String.format("Select EF 500%d", index), Hex.toHexString(response));
APDU = createSecureAPDU(new byte[0], read);
response = idCard.transceive(APDU);
Log.i(String.format("Read binary EF 500%d", index), Hex.toHexString(response));
FID[1] = index;
// store the decrypted datum
byte[] raw = encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(raw).lastIndexOf("80") / 2;
personalData[i] = new String(Arrays.copyOfRange(raw, 0, indexOfTerminator));
byte[] response = readFile(FID, "a personal data EF");
int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2;
personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator));
}
return personalData;
}
/**
* Attempts to verify the selected PIN
*
* @param PIN user-provided PIN
* @param oneOrTwo true for PIN1, false for PIN2
*/
private void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
selectFile(IASECCFID, "the master application");
if (!oneOrTwo) {
selectFile(QSCD, "the application");
}
// pad the PIN and use the chip for verification
byte[] paddedPIN = Hex.decode("ffffffffffffffffffffffff");
System.arraycopy(PIN, 0, paddedPIN, 0, PIN.length);
byte[] response = getResponse(paddedPIN, oneOrTwo ? verifyPIN1 : verifyPIN2, "PIN verification");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
if (response[response.length - 2] == 0x69 && response[response.length - 1] == (byte) 0x83) {
throw new RuntimeException("Invalid PIN. Authentication method blocked.");
} else {
throw new RuntimeException(String.format("Invalid PIN. Attempts left: %d.", response[response.length - 1] + 64));
}
}
}
/**
* Retrieves the authentication or signature certificate from the chip
*
* @param authOrSign true for auth, false for sign cert
* @return the requested certificate
*/
public byte[] getCertificate(boolean authOrSign) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
selectFile(IASECCFID, "the master application");
selectFile(authOrSign ? AWP : QSCD, "the application");
selectFile(authOrSign ? authCert : signCert, "the certificate");
byte[] certificate = new byte[0];
byte[] readCert = Arrays.copyOf(readFile, readFile.length);
// Construct the certificate byte array n=indexOfTerminator bytes at a time
for (int i = 0; i < 16; i++) {
// Set the P1/P2 values to incrementally read the certificate
readCert[2] = (byte) (certificate.length / 256);
readCert[3] = (byte) (certificate.length % 256);
byte[] response = getResponse(new byte[0], readCert, "Read the certificate");
if (response[response.length - 2] == 0x6b && response[response.length - 1] == 0x00) {
throw new RuntimeException("Wrong read parameters.");
}
// Set the range containing a portion of the certificate and decrypt it
int start = response[2] == 1 ? 3 : 4;
int end = start + (response[start - 2] + 256) % 256 - 1;
byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, start, end), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2;
certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator);
System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator);
if (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == 0x00) {
break;
}
}
return certificate;
}
/**
* Signs the authentication token hash
*
* @param PIN1 PIN1
* @param token the token hash to be signed
* @return authentication token hash signature
*/
public byte[] authenticate(String PIN1, byte[] token) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
verifyPIN(PIN1.getBytes(StandardCharsets.UTF_8), true);
selectFile(AWP, "the AWP application");
byte[] response = getResponse(Env, MSESetEnv, "Set environment");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException("Setting the environment failed.");
}
InternalAuthenticate[4] = (byte) (0x1d + 16 * (token.length / 16));
InternalAuthenticate[6] = (byte) (0x11 + 16 * (token.length / 16));
response = getResponse(token, InternalAuthenticate, "Internal Authenticate");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException("Signing the token failed.");
}
byte[] signature = encryptDecryptData(Arrays.copyOfRange(response, 3, 115), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(signature).lastIndexOf("80") / 2;
return Arrays.copyOf(signature, indexOfTerminator);
}
private byte[] getResponse(byte[] data, byte[] command, String log) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
byte[] response = idCard.transceive(createSecureAPDU(data, command));
Log.i(log, Hex.toHexString(response));
return response;
}
}

View File

@ -0,0 +1,73 @@
package com.tarkvaraprojekt.mobileauthapp
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPin2Binding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
/**
* Fragment that deals with asking PIN 2 from the user. Basically the same as PIN 1 fragment.
*/
class Pin2Fragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var _binding: FragmentPin2Binding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentPin2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.nextButton.setOnClickListener {
checkPin2Length()
}
binding.cancelButton.setOnClickListener {
cancel()
}
}
/**
* Checks if the length of the entered PIN 2 is in range 5..12 and if it is
* then it is saved to the viewModel.
*/
private fun checkPin2Length() {
val enteredPin2 = binding.pin2EditText.editText?.text.toString()
if (enteredPin2.length in 5..12) {
viewModel.setUserPin2(enteredPin2)
} else {
Toast.makeText(requireContext(), getString(R.string.length_pin2), Toast.LENGTH_SHORT)
.show()
}
}
/**
* Authentication process is cancelled when cancel button is clicked and the application
* will be closed.
*/
private fun cancel() {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}

View File

@ -1,67 +1,158 @@
package com.tarkvaraprojekt.mobileauthapp
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
/**
* Fragment that deals with asking the user for PIN1
* Fragment that deals with asking the user for PIN 1. If the user has already saved the PIN 1 then it is not asked again
* and the fragment is skipped and if the PIN 1 is not saved then the user is asked whether it should be saved or
* not before continuing.
*/
class PinFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentPinBinding? = null
private var _binding: FragmentPinBinding? = null
private val binding get() = _binding!!
// Navigation arguments:
// saving = true means that the user must be returned to the settings menu
private val args: PinFragmentArgs by navArgs()
private var saveToggle = true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentPinBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentPinBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.nextButton.setOnClickListener { goToNextFragment() }
binding!!.cancelButton.setOnClickListener { goToTheStart() }
// Currently PIN 1 is not required and thus this step is immediately skipped.
// In the future the UI flow will be changed in the nav_graph.
goToNextFragment()
checkIfSkip()
// Switch should be not visible when user is in savings mode
if (args.saving) {
binding.savePinQuestion.visibility = View.GONE
binding.saveLayout.visibility = View.GONE
} else {
saveToggle =
activity?.getPreferences(Context.MODE_PRIVATE)?.getBoolean("saveToggle", true) == true //Android Studio recommendation to get rid of Boolean?.
if (!saveToggle) {
binding.saveSwitch.isChecked = false
}
binding.saveSwitch.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
binding.saveStatus.text = getString(R.string.pin_save_on)
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", true)?.apply()
} else {
binding.saveStatus.text = getString(R.string.pin_save_off)
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", false)?.apply()
}
saveToggle = !saveToggle
}
}
binding.buttonContinue.setOnClickListener { checkEnteredPin() }
binding.buttonCancel.setOnClickListener { goToTheStart() }
}
private fun goToNextFragment() {
val enteredPin1 = binding!!.pinEditText.editText?.text.toString()
if (enteredPin1.length in 4..12) {
viewModel.setUserPin(
binding!!.pinEditText.editText?.text.toString()
)
findNavController().navigate(R.id.action_pinFragment_to_canFragment)
/**
* Takes user to the next fragment, which is AuthFragment.
*/
private fun goToTheNextFragment() {
val action = PinFragmentDirections.actionPinFragmentToAuthFragment(auth = args.auth, mobile = args.mobile)
findNavController().navigate(action)
}
/**
* Returns user to the start. If the user arrived from the settings menu then the start is
* settings menu not the HomeFragment.
*/
private fun goToTheStart() {
if (args.saving) {
findNavController().navigate(R.id.action_pinFragment_to_settingsFragment)
} else if (args.auth || args.mobile) {
if (args.mobile) {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
(activity as MainActivity).returnError(444)
requireActivity().finishAndRemoveTask()
}
} else {
// Currently it is not important to enter PIN1 so we will allow the user to leave this field empty
//Toast.makeText(requireContext(), getString(R.string.length_pin), Toast.LENGTH_SHORT)
// .show()
viewModel.setUserPin("1234")
findNavController().navigate(R.id.action_pinFragment_to_canFragment)
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
}
}
private fun goToTheStart() {
viewModel.clearUserInfo()
findNavController().navigate(R.id.action_pinFragment_to_homeFragment)
/**
* Checks if the current fragment can be skipped or not.
* If the user has PIN 1 saved on the device or PIN 1 is not required
* then the PIN 1 won't be asked.
*/
private fun checkIfSkip() {
if (viewModel.userPin.length in 4..12) {
goToTheNextFragment()
}
}
/**
* Method that creates and shows a snackbar that tells the user that PIN 1 has been saved
*/
private fun showSnackbar() {
val snackbar = Snackbar.make(requireView(), R.string.pin_status_saved, Snackbar.LENGTH_SHORT)
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
snackbar.show()
}
/**
* Checks whether the user has entered a PIN 1 with length between [4, 12] in the
* input field. If yes then the user is allowed to continue otherwise the user is
* allowed to modify the entered PIN 1.
*/
private fun checkEnteredPin() {
val enteredPin = binding.pinTextField.editText?.text.toString()
if (enteredPin.length in 4..12) {
viewModel.setUserPin(enteredPin)
if (args.saving) {
viewModel.storePin(requireContext())
showSnackbar()
goToTheStart()
} else {
if (saveToggle) {
viewModel.storePin(requireContext())
showSnackbar()
}
goToTheNextFragment()
}
} else {
Toast.makeText(requireContext(), getString(R.string.pin_helper_text), Toast.LENGTH_SHORT)
.show()
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@ -0,0 +1,113 @@
package com.tarkvaraprojekt.mobileauthapp
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.koushikdutta.ion.Ion
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import org.json.JSONObject
/**
* ResultFragment is used to create a JWT and to send response to the website/application
* that launched the MobileAuthApp. If the mobile auth app was started by a website
* the result is sent to a server with a POST request.
*/
class ResultFragment : Fragment() {
private val paramsModel: ParametersViewModel by activityViewModels()
private var _binding: FragmentResultBinding? = null
private val binding get() = _binding!!
private val args: ResultFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentResultBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postToken()
}
/**
* Only used when the MobileAuthApp was launched by an app. Not for website use.
* Not really the safest way of doing things, but sufficient for POC purposes.
*/
private fun createResponse(
success: Boolean = true,
idCode: String = "noCode",
name: String = "noName",
authority: String = "noAuthority"
) {
val responseCode =
if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
val resultIntent = Intent()
resultIntent.putExtra("idCode", idCode)
resultIntent.putExtra("name", name)
resultIntent.putExtra("authority", authority)
requireActivity().setResult(responseCode, resultIntent)
requireActivity().finish()
}
/**
* Makes a POST request to the backend server with a tokenItem
*/
fun postToken() {
val json = JsonObject()
json.addProperty("auth-token", paramsModel.token)
json.addProperty("error", 200)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
val ion = Ion.with(activity)
.load(paramsModel.authUrl)
for ((header, value) in paramsModel.headers) {
ion.setHeader(header, value)
}
ion
.setJsonObjectBody(json)
.asJsonObject()
.setCallback { e, result ->
Log.i("resultTag", result.toString())
if (result == null) {
if (args.mobile) {
createResponse(false)
} else {
requireActivity().finishAndRemoveTask()
}
} else {
if (args.mobile) {
val userData = result.asJsonObject["userData"]
val idCode = userData.asJsonObject["idCode"].asString
val name = userData.asJsonObject["name"].asString
val authority = result.asJsonObject["roles"].asJsonArray[0].asJsonObject["authority"].asString
createResponse(true, idCode, name, authority)
} else {
requireActivity().finishAndRemoveTask()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}

View File

@ -20,26 +20,39 @@ class UserFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentUserBinding? = null
private var _binding: FragmentUserBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentUserBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentUserBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.userName.text =
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
binding!!.identificationNumber.text = viewModel.userIdentificationNumber
binding!!.clearButton.setOnClickListener { goToTheStart() }
displayInformation()
binding.clearButton.setOnClickListener { goToTheStart() }
}
/**
* Assigns text values to the fields in order to display user information.
*/
private fun displayInformation() {
binding.userName.text =
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
binding.identificationNumber.text = viewModel.userIdentificationNumber
binding.gender.text = viewModel.gender
binding.expiration.text = viewModel.expiration.replace(" ", "/")
binding.citizenship.text = viewModel.citizenship
}
/**
* Navigates user back to the start and also deletes any temporary information.
*/
private fun goToTheStart() {
viewModel.clearUserInfo()
findNavController().navigate(R.id.action_userFragment_to_homeFragment)
@ -47,6 +60,6 @@ class UserFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@ -1,22 +1,59 @@
package com.tarkvaraprojekt.mobileauthapp.auth
import android.nfc.tech.IsoDep
import android.util.Log
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import java.math.BigInteger
import io.jsonwebtoken.SignatureAlgorithm
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.LocalDateTime
import java.time.ZoneOffset
class Authenticator(val comms : Comms) {
class Authenticator(val comms: Comms) {
public fun authenticate(nonce: BigInteger, challengeUrl: String, pin1: String) {
val type = "JWT"
val algorithm = "ES384"
var iss = "https://self-issued.me" // Will be specified at a later date.
val algorithmUsedForSigning = SignatureAlgorithm.ES384
fun authenticate(challenge: String, originUrl: String, pin1: String): String {
// Ask PIN 1 from the user and get the authentication certificate from the ID card.
val authenticationCertificate : ByteArray = comms.getAuthenticationCertificate();
val authenticationCertificate: ByteArray = comms.getCertificate(true);
// Create the authentication token (OpenID X509)
// Encode the certificate in base64.
val base64cert = java.util.Base64.getEncoder().encodeToString(authenticationCertificate)
// Hash the authentication token.
// Get current epoch time.
val epoch = LocalDateTime.now(ZoneOffset.UTC).atZone(ZoneOffset.UTC).toEpochSecond()
// Get expiration time.
val exp = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(5 * 60L).atZone(ZoneOffset.UTC)
.toEpochSecond()
// TODO: Get subject value.
val sub = "FAMILYNAME.NAME"
// Get header and claims.
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
val claims =
"""{"iat":"$epoch","exp":"$exp","aud":["$originUrl"],"iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
claims.toByteArray(Charsets.UTF_8)
)
// Send the authentication token hash to the ID card for signing and get signed authentication token as response.
val encoded =
MessageDigest.getInstance("SHA-384").digest(jwt.toByteArray(StandardCharsets.UTF_8))
val signed = comms.authenticate(pin1, encoded)
// Return the signed authentication token.
return jwt + "." + base64Encode(signed)
}
fun base64Encode(bytes: ByteArray): String? {
val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes)
return encoded.replace("=", "")
}
}

View File

@ -0,0 +1,161 @@
package com.tarkvaraprojekt.mobileauthapp.menu
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import com.tarkvaraprojekt.mobileauthapp.MainActivity
import com.tarkvaraprojekt.mobileauthapp.R
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentSettingsBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
/**
* This fragment allows the user to save the CAN and the PIN 1 and also to delete them if necessary.
* Should only be accessible for the user from the HomeFragment and not during the
* authentication process itself.
*/
class SettingsFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private var showPin: Boolean = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showCanField()
showPinField()
togglePinButton()
binding.canMenuAction.setOnClickListener { canAction() }
binding.pinMenuAction.setOnClickListener { pinAction() }
binding.pinMenuShow.setOnClickListener { togglePin() }
binding.returnButton.setOnClickListener { backToHome() }
}
/**
* Method for showing a snackbar with a message that is given as a parameter
*/
private fun showSnackbar(message: String) {
val snackbar = Snackbar.make(requireView(), message, Snackbar.LENGTH_SHORT)
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
snackbar.show()
}
/**
* Method for showing the CAN field to the user and can be used to refresh the field as well.
*/
private fun showCanField() {
if (viewModel.userCan.length == 6) {
binding.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
binding.canMenuAction.text = getString(R.string.can_delete)
} else {
binding.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
binding.canMenuAction.text = getString(R.string.add_can_text)
}
}
/**
* Method that allows the user to delete saved CAN from the device and also to save new a CAN if
* currently there is no CAN saved.
*/
private fun canAction() {
if (viewModel.userCan.length == 6) {
viewModel.deleteCan(requireContext())
showCanField()
showSnackbar(getString(R.string.can_deleted))
} else {
(activity as MainActivity).inMenu = false
val action = SettingsFragmentDirections.actionSettingsFragmentToCanFragment(saving = true)
findNavController().navigate(action)
}
}
/**
* Method for showing the PIN 1 field to the user and can be used to refresh the field as well.
* The PIN 1 is hidden by default and when it is hidden it is always shown as **** despite the
* length of the PIN 1. Can be made visible with toggle button.
*/
private fun showPinField() {
if (viewModel.userPin.length in 4..12) {
binding.pinMenuShow.visibility = Button.VISIBLE
if (showPin)
binding.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
else
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
binding.pinMenuAction.text = getString(R.string.pin1_delete)
} else {
binding.pinMenuShow.visibility = Button.GONE
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
binding.pinMenuAction.text = getString(R.string.pin1_add)
}
}
/**
* Method that allows the user to delete saved PIN 1 from the device and also to save a new PIN 1 if
* currently there is no PIN 1 saved.
*/
private fun pinAction() {
if (viewModel.userPin.length in 4..12) {
viewModel.deletePin(requireContext())
showPinField()
showSnackbar(getString(R.string.pin_deleted))
} else {
(activity as MainActivity).inMenu = false
val action = SettingsFragmentDirections.actionSettingsFragmentToPinFragment(saving = true)
findNavController().navigate(action)
}
}
/**
* Hides the PIN 1 or makes it visible.
*/
private fun togglePin() {
showPin = !showPin
togglePinButton()
showPinField()
}
/**
* Updates the text on the button that controls the visiblity of the PIN 1.
*/
private fun togglePinButton() {
if (showPin) {
binding.pinMenuShow.text = getString(R.string.hide)
} else {
binding.pinMenuShow.text = getString(R.string.show)
}
}
/**
* Navigates back to home fragment.
*/
private fun backToHome() {
(activity as MainActivity).inMenu = false
findNavController().navigate(R.id.action_settingsFragment_to_homeFragment)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}

View File

@ -0,0 +1,44 @@
package com.tarkvaraprojekt.mobileauthapp.model
import android.util.Log
import android.util.Log.WARN
import androidx.lifecycle.ViewModel
class ParametersViewModel: ViewModel() {
private var _challenge: String = ""
val challenge get() = _challenge
private var _authUrl: String = ""
val authUrl get() = _authUrl
private var _token: String = ""
val token get() = _token
private var _origin: String = ""
val origin get() = _origin
private var _headers: Map<String, String> = HashMap<String, String>()
val headers get() =_headers
fun setChallenge(newChallenge: String) {
_challenge = newChallenge
}
fun setAuthUrl(newAuthUrl: String) {
_authUrl = newAuthUrl
}
fun setToken(newToken: String) {
_token = newToken
}
fun setOrigin(newOrigin: String) {
_origin = newOrigin
}
fun setHeaders(newHeaders: Map<String, String>) {
Log.i("HEADERS", newHeaders.toList().toString())
_headers = newHeaders
}
}

View File

@ -1,12 +1,19 @@
package com.tarkvaraprojekt.mobileauthapp.model
import android.content.Context
import android.content.SharedPreferences
import androidx.lifecycle.ViewModel
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
class SmartCardViewModel: ViewModel() {
private var _userPin: String = ""
val userPin get() = _userPin
private var _userPin2: String = ""
val userPin2 get() = _userPin2
private var _userCan: String = ""
val userCan get() = _userCan
@ -19,18 +26,35 @@ class SmartCardViewModel: ViewModel() {
private var _userIdentificationNumber: String = ""
val userIdentificationNumber get() = _userIdentificationNumber
private var _gender: String = ""
val gender get() = _gender
private var _expiration: String = ""
val expiration get() = _expiration
private var _citizenship: String = ""
val citizenship get() = _citizenship
fun clearUserInfo() {
_userPin = ""
_userPin2 = ""
_userCan = ""
_userFirstName = ""
_userLastName = ""
_userIdentificationNumber = ""
_expiration = ""
_citizenship = ""
_gender = ""
}
fun setUserPin(newUserPin: String) {
_userPin = newUserPin
}
fun setUserPin2(newUserPin2: String) {
_userPin2 = newUserPin2
}
fun setUserCan(newUserCan: String) {
_userCan = newUserCan
}
@ -47,4 +71,67 @@ class SmartCardViewModel: ViewModel() {
_userIdentificationNumber = newUserIdentificationNumber
}
fun setExpiration(newExpiration: String) {
_expiration = newExpiration
}
fun setCitizenship(newCitizenship: String) {
_citizenship = newCitizenship
}
fun setGender(newGender: String) {
_gender = newGender
}
private fun getSharedPreferences(context: Context): SharedPreferences {
val masterKeyAlias: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
return EncryptedSharedPreferences.create(
"user_creds",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
fun storeCan(context: Context) {
val sharedPreferences: SharedPreferences = getSharedPreferences(context)
sharedPreferences.edit().putString("CAN", userCan).apply()
}
fun checkCan(context: Context) {
val sharedPreferences: SharedPreferences = getSharedPreferences(context)
val foundCan = sharedPreferences.getString("CAN", null)
foundCan?.let {
_userCan = it
}
}
// Must be called from AuthFragment as well, when CAN is wrong.
fun deleteCan(context: Context) {
val sharedPreferences: SharedPreferences = getSharedPreferences(context)
sharedPreferences.edit().remove("CAN").apply()
_userCan = ""
}
fun storePin(context: Context) {
val sharedPreferences: SharedPreferences = getSharedPreferences(context)
sharedPreferences.edit().putString("PIN1", userPin).apply()
}
fun checkPin(context: Context) {
val sharedPreferences: SharedPreferences = getSharedPreferences(context)
val foundPin = sharedPreferences.getString("PIN1", null)
foundPin?.let {
_userPin = it
}
}
fun deletePin(context: Context) {
val sharedPreferences: SharedPreferences = getSharedPreferences(context)
sharedPreferences.edit().remove("PIN1").apply()
_userPin = ""
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- For material card views as recommended in the material.io website -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
<item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_checked="false"/>
</selector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -5,7 +5,7 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:fillColor="#001970"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@ -6,12 +6,28 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
app:navGraph="@navigation/nav_graph"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,47 +4,51 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
android:padding="@dimen/padding"
tools:context=".AuthFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_margin="@dimen/margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20sp">
android:padding="@dimen/padding_small">
<TextView
android:id="@+id/auth_fragment_instruction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:gravity="center"
android:layout_margin="@dimen/margin"
android:gravity="left"
android:text="@string/auth_instruction_text"
android:textSize="20sp" />
android:textSize="@dimen/regular_text" />
<ImageView
android:id="@+id/nfc_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="@dimen/logo_big"
android:layout_height="@dimen/logo_big"
android:layout_gravity="center"
android:layout_margin="6dp"
android:layout_margin="@dimen/margin"
android:padding="@dimen/margin_huge"
android:src="@drawable/nfc_logo" />
<TextView
android:id="@+id/time_counter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:textSize="14sp"
android:layout_margin="@dimen/margin"
android:textSize="@dimen/regular_text"
app:layout_constraintTop_toBottomOf="@id/auth_fragment_instruction"
tools:text="@string/time_left" />
@ -57,9 +61,9 @@
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/next_text"
android:textSize="18sp"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/continue_button"
android:textSize="@dimen/regular_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" />
@ -68,10 +72,10 @@
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="@dimen/margin_big"
android:layout_marginStart="@dimen/padding_tiny"
android:text="@string/cancel_text"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@id/next_button"
android:textSize="@dimen/regular_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />

View File

@ -4,79 +4,57 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".CanFragment">
android:padding="@dimen/padding"
tools:context=".MainActivity">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
<TextView
android:id="@+id/title_text"
android:text="@string/can_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:textSize="@dimen/headline_text"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/canTextField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:hint="@string/can_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_text"
app:helperTextEnabled="true"
app:helperText="@string/can_helper_text"
app:helperTextTextAppearance="@style/helper"
app:counterEnabled="true"
app:counterMaxLength="6"
app:counterTextAppearance="@style/helper"
app:counterOverflowTextAppearance="@style/helper"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20sp">
android:textSize="@dimen/regular_text"
android:fontFamily="sans-serif"
android:inputType="number"
android:singleLine="true"
/>
<TextView
android:id="@+id/enter_can"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:text="@string/enter_can"
android:textSize="20sp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/can_edit_text"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:hint="@string/text_can"
app:counterEnabled="true"
app:counterMaxLength="6"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:helperText="@string/example_can"
app:helperTextEnabled="true"
app:startIconDrawable="@drawable/can_logo">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/next_text"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" />
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:id="@+id/button_cancel"
android:text="@string/cancel_text"
android:textSize="18dp"
app:layout_constraintEnd_toStartOf="@id/next_button"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />
app:layout_constraintTop_toBottomOf="@id/canTextField" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,27 +4,125 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
android:padding="@dimen/padding"
tools:context=".HomeFragment">
<TextView
android:id="@+id/home_fragment_text"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/saved_states"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_fragment"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/can_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/can_status_logo"
android:layout_marginStart="@dimen/margin"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/can_status_text"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/pin_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/pin_status_logo"
android:layout_marginStart="@dimen/margin"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/pin_status_text"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
android:id="@+id/id_card_detection"
android:layout_margin="@dimen/margin_big"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/saved_states"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/detection_action_text"
android:layout_margin="@dimen/margin_big"
android:textSize="@dimen/regular_text"
android:text="@string/action_detect"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- Temporary button for testing purposes -->
<Button
android:id="@+id/begin_button"
android:id="@+id/home_action_button"
android:textSize="@dimen/regular_text"
android:text="@string/try_again_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/begin_text"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/home_fragment_text"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_huge"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
app:layout_constraintTop_toBottomOf="@id/id_card_detection"/>
<Button
android:id="@+id/home_help_button"
android:textSize="@dimen/regular_text"
android:text="@string/help_text"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_huge"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="?attr/materialButtonOutlinedStyle"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/home_action_button"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,76 +4,111 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".PinFragment">
android:padding="@dimen/padding"
tools:context=".MainActivity">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
<TextView
android:id="@+id/title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
android:text="@string/pin_view"
android:textSize="@dimen/headline_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pinTextField"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:hint="@string/hint_pin"
app:counterEnabled="true"
app:counterMaxLength="12"
app:counterOverflowTextAppearance="@style/helper"
app:counterTextAppearance="@style/helper"
app:endIconMode="password_toggle"
app:helperText="@string/pin_helper_text"
app:helperTextEnabled="true"
app:helperTextTextAppearance="@style/helper"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_text">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
android:fontFamily="sans-serif"
android:inputType="numberPassword"
android:singleLine="true"
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/pin_fragment_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:text="@string/pin_fragment" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pin_edit_text"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:hint="@string/enter_pin"
app:counterEnabled="true"
app:counterMaxLength="12"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:helperText="@string/example_pin"
app:helperTextEnabled="true"
app:startIconDrawable="@drawable/can_logo">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/next_button"
<TextView
android:id="@+id/save_pin_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/next_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" />
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/cancel_text"
app:layout_constraintEnd_toStartOf="@id/next_button"
android:layout_margin="@dimen/margin_big"
android:paddingTop="@dimen/padding"
android:fontFamily="sans-serif"
android:text="@string/save_pin"
android:textSize="@dimen/regular_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />
app:layout_constraintTop_toBottomOf="@id/pinTextField" />
<LinearLayout
android:id="@+id/save_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/save_pin_question">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/save_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:checked="true"
android:minWidth="48dp"
android:minHeight="48dp"
android:layout_gravity="center_vertical"/>
<TextView
android:id="@+id/save_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
android:text="@string/pin_save_on"
android:textSize="@dimen/regular_text"
android:layout_gravity="center_vertical"/>
</LinearLayout>
<Button
android:id="@+id/button_continue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
android:text="@string/continue_button"
android:textSize="@dimen/regular_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/save_layout" />
<Button
android:id="@+id/button_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
android:text="@string/cancel_text"
android:textSize="@dimen/regular_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_continue" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".Pin2Fragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/pin2_fragment_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:text="@string/pin2_fragment" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pin2_edit_text"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:hint="@string/enter_pin2"
app:counterEnabled="true"
app:counterMaxLength="12"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:helperText="@string/example_pin2"
app:helperTextEnabled="true"
app:startIconDrawable="@drawable/can_logo">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/continue_button"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" />
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/cancel_text"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/padding"
tools:context=".ResultFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/can_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/result_text"
android:text="@string/result_text"
android:textSize="@dimen/regular_text"
android:padding="@dimen/padding_small"
android:layout_marginVertical="@dimen/margin_small"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/result_info_text"
android:text="@string/result_info"
android:padding="@dimen/padding_small"
android:textSize="@dimen/regular_text"
android:layout_marginVertical="@dimen/margin_small"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/padding"
tools:context=".menu.SettingsFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/settings_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:id="@+id/can_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/padding_small">
<TextView
android:id="@+id/can_saved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin_small"
android:text="@string/saved_can" />
<Button
android:id="@+id/can_menu_action"
android:layout_margin="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/pin_saved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin_small"
android:text="@string/saved_pin"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/pin_menu_show"
android:layout_marginHorizontal="@dimen/margin"
android:layout_marginVertical="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
<Button
android:id="@+id/pin_menu_action"
android:layout_marginHorizontal="@dimen/margin"
android:layout_marginVertical="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/return_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/return_text"
android:layout_marginVertical="@dimen/margin"
android:layout_marginStart="@dimen/padding"
android:textSize="@dimen/regular_text"
app:layout_constraintTop_toBottomOf="@id/settings_card"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,72 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".UserFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent"
android:padding="@dimen/padding"
tools:context=".UserFragment">
<LinearLayout
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20sp">
android:layout_margin="@dimen/margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<TextView
android:id="@+id/user_name_label"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/user_name_label"
android:textSize="14sp" />
android:orientation="vertical"
android:padding="@dimen/padding_tiny">
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/user_name"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/user_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/user_name_label"
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/identification_number_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/identification_number_label"
android:textSize="14sp" />
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/user_name"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
<TextView
android:id="@+id/identification_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/identification_number_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/identification_number_label"
android:textSize="@dimen/regular_text" />
</LinearLayout>
<TextView
android:id="@+id/identification_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/gender_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/gender_label"
android:textSize="@dimen/regular_text" />
<Button
android:id="@+id/clear_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/clear_button"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />
<TextView
android:id="@+id/gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/expiration_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/expiration_label"
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/expiration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
<TextView
android:id="@+id/citizenship_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/citizenship_label"
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/citizenship"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/clear_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/return_text"
android:textSize="@dimen/regular_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_settings_option"
android:title="@string/menu_settings_title"
android:icon="@drawable/ic_settings"
app:showAsAction="always"/>
</menu>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_check_logo" />
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_check_logo" />
</adaptive-icon>

View File

@ -9,66 +9,150 @@
android:id="@+id/homeFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.HomeFragment"
android:label="fragment_home"
tools:layout="@layout/fragment_home" >
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_homeFragment_to_pinFragment"
app:destination="@id/pinFragment"
/>
android:id="@+id/action_homeFragment_to_settingsFragment"
app:destination="@id/settingsFragment"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_homeFragment_to_canFragment"
app:destination="@id/canFragment"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_homeFragment_to_userFragment"
app:destination="@id/userFragment" />
</fragment>
<fragment
android:id="@+id/pinFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.PinFragment"
android:label="fragment_pin"
tools:layout="@layout/fragment_pin" >
<action
android:id="@+id/action_pinFragment_to_canFragment"
app:destination="@id/canFragment"
app:popUpTo="@id/homeFragment" />
tools:layout="@layout/fragment_pin">
<action
android:id="@+id/action_pinFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_pinFragment_to_settingsFragment"
app:destination="@id/settingsFragment"
app:popUpTo="@id/settingsFragment"
app:popUpToInclusive="true" />
<argument
android:name="saving"
android:defaultValue="false"
app:argType="boolean" />
<action
android:id="@+id/action_pinFragment_to_authFragment"
app:destination="@id/authFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/canFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.CanFragment"
android:label="fragment_can"
tools:layout="@layout/fragment_can" >
<action
android:id="@+id/action_canFragment_to_authFragment"
app:destination="@id/authFragment"
app:popUpTo="@id/homeFragment"/>
tools:layout="@layout/fragment_can">
<action
android:id="@+id/action_canFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_canFragment_to_settingsFragment"
app:destination="@id/settingsFragment"
app:popUpTo="@id/settingsFragment"
app:popUpToInclusive="true" />
<argument
android:name="saving"
android:defaultValue="false"
app:argType="boolean" />
<action
android:id="@+id/action_canFragment_to_pinFragment"
app:destination="@id/pinFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="fromhome"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/authFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.AuthFragment"
android:label="fragment_auth"
tools:layout="@layout/fragment_auth" >
tools:layout="@layout/fragment_auth">
<action
android:id="@+id/action_authFragment_to_userFragment"
app:destination="@id/userFragment"
app:popUpTo="@id/homeFragment"/>
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_authFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_authFragment_to_resultFragment"
app:destination="@id/resultFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/userFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.UserFragment"
android:label="fragment_user"
tools:layout="@layout/fragment_user" >
tools:layout="@layout/fragment_user">
<action
android:id="@+id/action_userFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true"/>
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.menu.SettingsFragment"
android:label="fragment_settings"
tools:layout="@layout/fragment_settings">
<action
android:id="@+id/action_settingsFragment_to_canFragment"
app:destination="@id/canFragment" />
<action
android:id="@+id/action_settingsFragment_to_pinFragment"
app:destination="@id/pinFragment" />
<action
android:id="@+id/action_settingsFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/resultFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.ResultFragment"
android:label="fragment_result"
tools:layout="@layout/fragment_result" >
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
</navigation>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Must translate to English, but should work now -->
<string name="app_name">NFC authenticator</string>
<!-- BUTTONS -->
<string name="cancel_text">CANCEL</string>
<string name="return_text">BACK</string>
<string name="add_can_text">ADD CAN</string>
<string name="try_again_text">TRY AGAIN</string>
<string name="continue_button">CONTINUE</string>
<!-- Card Detection related -->
<string name="card_detected">Card detected. Hold it against the phone.</string>
<string name="data_read">Data read. You can continue.</string>
<string name="wrong_can_text">Wrong CAN</string>
<string name="action_detect">Put the ID card against the phone to detect it</string>
<string name="action_detect_unavailable">CAN must be added before ID card can be detected</string>
<string name="nfc_not_available">NFC is not turned on or is not supported by the phone</string>
<string name="nfc_reading_error">The provided CAN does not match the ID card</string>
<string name="id_card_removed_early">ID card was removed too early</string>
<string name="wrong_pin">Wrong PIN 1. Tries on the card left %s</string>
<!-- string resources for HomeFragment -->
<string name="pin_status_saved">PIN 1 saved</string>
<string name="pin_status_negative">PIN 1 not saved</string>
<string name="can_status_saved">CAN saved</string>
<string name="can_status_negative">CAN not saved</string>
<string name="help_text">HELP</string>
<string name="can_question">What is CAN?</string>
<string name="can_explanation">CAN is a 6 digit code that is needed to communicate with an ID card. It can be found on the ID card under the card holder\'s picture with a title KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
<string name="problem_parameters">Problem with parameters</string>
<string name="problem_challenge">Challenge is missing</string>
<string name="problem_authurl">AuthUrl is missing</string>
<string name="problem_originurl">OriginUrl is missing</string>
<string name="problem_other">Unspecified problem with parameters</string>
<!-- string resources for PinFragment -->
<string name="pin_view">Please enter PIN 1</string>
<string name="hint_pin">PIN 1</string>
<string name="pin_helper_text">PIN 1 must be 412 digits long</string>
<string name="save_pin">Save PIN 1</string>
<string name="pin_save_on">On</string>
<string name="pin_save_off">Off</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Please enter PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Example. 123456</string>
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
<!-- string resources for CanFragment -->
<string name="can_view">Please enter CAN</string>
<string name="can_text">CAN</string>
<string name="can_helper_text">CAN must be 6 digits long</string>
<!-- string resources for AuthFragment layout -->
<string name="auth_instruction_text">Put the ID card against the phone</string>
<string name="time_left">Time left %d sek</string>
<string name="no_time">No time left</string>
<!-- string resources for UserFragment layout -->
<string name="user_name_label">NAME</string>
<string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">IDENTIFICATION NUMBER</string>
<string name="expiration_label">DATE OF EXPIRY</string>
<string name="citizenship_label">CITIZENSHIP</string>
<string name="gender_label">SEX</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">Checking the created token</string>
<string name="result_info">The app will close automatically</string>
<!-- menu -->
<string name="menu_settings_title">Settings</string>
<string name="saved_can">CAN: %s</string>
<string name="can_delete">Delete CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Add PIN1</string>
<string name="pin1_delete">Delete PIN1</string>
<string name="missing">not saved</string>
<string name="show">SHOW</string>
<string name="hide">HIDE</string>
<string name="hidden_pin">****</string>
<string name="menu_unavailable_message">Settings are currently unavailable</string>
<string name="can_deleted">CAN deleted</string>
<string name="pin_deleted">PIN 1 deleted</string>
</resources>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NFC autentija</string>
<!-- Buttons -->
<string name="cancel_text">KATKESTA</string>
<string name="return_text">TAGASI</string>
<string name="add_can_text">LISA CAN</string>
<string name="try_again_text">ÜRITA UUESTI</string>
<string name="continue_button">JÄTKA</string>
<!-- Card Detection related -->
<string name="card_detected">Kaart tuvastatud. Hoia kaarti vastu telefoni.</string>
<string name="data_read">Andmed loetud, võid jätkata.</string>
<string name="wrong_can_text">Vale CAN</string>
<string name="action_detect">ID kaardi tuvastamiseks pane kaart vastu telefoni</string>
<string name="action_detect_unavailable">ID kaardi tuvastamiseks peab olema CAN lisatud</string>
<string name="nfc_not_available">NFC ei ole sisse lülitatud või puudub telefonil NFC võimekus</string>
<string name="nfc_reading_error">Sisestatud CAN ei ole vastavuses ID kaardiga</string>
<string name="id_card_removed_early">ID kaart eemaldati liiga vara</string>
<string name="wrong_pin">Vale PIN 1. ID kaardil PIN 1 sisetamise kordi alles: %s</string>
<!-- string resources for HomeFragment -->
<string name="pin_status_saved">PIN 1 on salvestatud</string>
<string name="pin_status_negative">PIN 1 ei ole salvestatud</string>
<string name="can_status_saved">CAN on salvestatud</string>
<string name="can_status_negative">CAN ei ole salvestatud</string>
<string name="help_text">INFO</string>
<string name="can_question">Mis on CAN?</string>
<string name="can_explanation">CAN on 6 kohaline numbritest koosnev kood, mida on vaja ID kaardiga suhtlemiseks. CAN-i leiab ID kaardilt omaniku pildi alt pealkirjaga KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
<string name="problem_parameters">Probleem parameetritega</string>
<string name="problem_challenge">Puudub challenge parameeter</string>
<string name="problem_authurl">Puudub AuthUrl parameeter</string>
<string name="problem_originurl">Puudub OriginUrl parameeter</string>
<string name="problem_other">Täpsustamata probleem parameetritega</string>
<!-- string resources for PinFragment -->
<string name="pin_view">Palun sisesta PIN 1</string>
<string name="hint_pin">PIN 1</string>
<string name="pin_helper_text">PIN 1 lubatud pikkus on 4..12</string>
<string name="save_pin">Save PIN 1</string>
<string name="pin_save_on">On</string>
<string name="pin_save_off">Off</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Palun sisesta PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Näide. 123456</string>
<string name="length_pin2">PIN 2 lubatud pikkus on 5..12</string>
<!-- string resources for CanFragment -->
<string name="can_view">Please enter CAN</string>
<string name="can_text">CAN</string>
<string name="can_helper_text">CAN must be 6 digits long</string>
<!-- string resources for AuthFragment layout -->
<string name="auth_instruction_text">Pane ID kaart vastu telefoni</string>
<string name="time_left">Aega on jäänud %d sek</string>
<string name="no_time">Aeg on otsas</string>
<!-- string resources for UserFragment layout -->
<string name="user_name_label">NIMI</string>
<string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">ISIKUKOOD</string>
<string name="expiration_label">KEHTIV KUNI</string>
<string name="citizenship_label">KODAKONDSUS</string>
<string name="gender_label">SUGU</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">Tulemust kontrollitakse</string>
<string name="result_info">Rakendus sulgeb ennast ise</string>
<!-- menu -->
<string name="menu_settings_title">Seaded</string>
<string name="saved_can">CAN: %s</string>
<string name="can_delete">Kustuta CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Lisa PIN1</string>
<string name="pin1_delete">Kustuta PIN1</string>
<string name="missing">puudub</string>
<string name="show">NÄITA</string>
<string name="hide">PEIDA</string>
<string name="hidden_pin">****</string>
<string name="menu_unavailable_message">Seaded pole hetkel saadaval</string>
<string name="can_deleted">CAN kustatud</string>
<string name="pin_deleted">PIN 1 kustatud</string>
</resources>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MobileAuthApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.MobileAuthApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/blue_200</item>
<item name="colorPrimaryVariant">@color/blue_700</item>

View File

@ -8,10 +8,9 @@
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="blue_200">#90caf9</color>
<color name="blue_500">#2196f3</color>
<color name="blue_700">#1976d2</color>
<color name="orange_200">#ffcc80</color>
<color name="blue_200">#d1d9ff</color>
<color name="blue_500">#002984</color>
<color name="blue_700">#001970</color>
<color name="orange_200">#ffab91</color>
<color name="orange_700">#f57c00</color>
</resources>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="margin_small">4dp</dimen>
<dimen name="margin">8dp</dimen>
<dimen name="margin_big">16dp</dimen>
<dimen name="margin_huge">32dp</dimen>
<dimen name="padding_tiny">8dp</dimen>
<dimen name="padding_small">16dp</dimen>
<dimen name="padding">24dp</dimen>
<dimen name="regular_text">24sp</dimen>
<dimen name="headline_text">32sp</dimen>
<dimen name="helper_text">16sp</dimen>
<dimen name="small_text">8sp</dimen>
<dimen name="logo_big">128dp</dimen>
</resources>

View File

@ -1,34 +1,85 @@
<resources>
<string name="app_name">Mobile Authenticator</string>
<string name="home_fragment">Work in progress</string>
<string name="app_name">NFC authenticator</string>
<string name="begin_text">ALUSTA</string>
<string name="next_text">EDASI</string>
<string name="cancel_text">KATKESTA</string>
<!-- BUTTONS -->
<string name="cancel_text">CANCEL</string>
<string name="return_text">BACK</string>
<string name="add_can_text">ADD CAN</string>
<string name="try_again_text">TRY AGAIN</string>
<string name="continue_button">CONTINUE</string>
<!-- Card Detection related -->
<string name="card_detected">Card detected. Hold it against the phone.</string>
<string name="data_read">Data read. You can continue.</string>
<string name="wrong_can_text">Wrong CAN</string>
<string name="action_detect">Put the ID card against the phone to detect it</string>
<string name="action_detect_unavailable">CAN must be added before ID card can be detected</string>
<string name="nfc_not_available">NFC is not turned on or is not supported by the phone</string>
<string name="nfc_reading_error">The provided CAN does not match the ID card</string>
<string name="id_card_removed_early">ID card was removed too early</string>
<string name="wrong_pin">Wrong PIN 1. Tries on the card left %s</string>
<!-- string resources for HomeFragment -->
<string name="pin_status_saved">PIN 1 saved</string>
<string name="pin_status_negative">PIN 1 not saved</string>
<string name="can_status_saved">CAN saved</string>
<string name="can_status_negative">CAN not saved</string>
<string name="help_text">HELP</string>
<string name="can_question">What is CAN?</string>
<string name="can_explanation">CAN is a 6 digit code that is needed to communicate with an ID card. It can be found on the ID card under the card holder\'s picture with a title KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
<string name="problem_parameters">Problem with parameters</string>
<string name="problem_challenge">Challenge is missing</string>
<string name="problem_authurl">AuthUrl is missing</string>
<string name="problem_originurl">OriginUrl is missing</string>
<string name="problem_other">Unspecified problem with parameters</string>
<!-- string resources for PinFragment -->
<string name="pin_fragment">Palun sisesta PIN 1</string>
<string name="enter_pin">PIN 1</string>
<string name="example_pin">Näide. 1234</string>
<string name="length_pin">PIN 1 lubatud pikkus on 4..12</string>
<string name="pin_view">Please enter PIN 1</string>
<string name="hint_pin">PIN 1</string>
<string name="pin_helper_text">PIN 1 must be 412 digits long</string>
<string name="save_pin">Save PIN 1</string>
<string name="pin_save_on">On</string>
<string name="pin_save_off">Off</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Please enter PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Example. 123456</string>
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
<!-- string resources for CanFragment -->
<string name="example_can">Näide. 123456</string>
<string name="text_can">CAN</string>
<string name="enter_can">Sisesta ID kaardi CAN (Card Access Number)</string>
<string name="length_can">CANi pikkus on vale</string>
<string name="card_detected">Kaart on tuvastatud. Hoia kaarti vastu telefoni.</string>
<string name="data_read">Andmed loetud. Võid edasi minna.</string>
<string name="can_view">Please enter CAN</string>
<string name="can_text">CAN</string>
<string name="can_helper_text">CAN must be 6 digits long</string>
<!-- string resources for AuthFragment layout -->
<string name="auth_instruction_text">ID kaardiga ühenduse loomiseks pane kaart vastu telefoni</string>
<string name="time_left">Aega on jäänud %d sek</string>
<string name="no_time">Aeg on otsas</string>
<string name="no_success">Vale CAN</string>
<string name="auth_instruction_text">Put the ID card against the phone</string>
<string name="time_left">Time left %d sek</string>
<string name="no_time">No time left</string>
<!-- string resources for UserFragment layout -->
<string name="user_name_label">NIMI</string>
<string name="user_name_label">NAME</string>
<string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">ISIKUKOOD</string>
<string name="clear_button">UNUSTA</string>
<string name="identification_number_label">IDENTIFICATION NUMBER</string>
<string name="expiration_label">DATE OF EXPIRY</string>
<string name="citizenship_label">CITIZENSHIP</string>
<string name="gender_label">SEX</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">Controlling the created token</string>
<string name="result_info">The app will close automatically</string>
<!-- menu -->
<string name="menu_settings_title">Settings</string>
<string name="saved_can">CAN: %s</string>
<string name="can_delete">Delete CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Add PIN 1</string>
<string name="pin1_delete">Delete PIN 1</string>
<string name="missing">not saved</string>
<string name="show">SHOW</string>
<string name="hide">HIDE</string>
<string name="hidden_pin">****</string>
<string name="menu_unavailable_message">Settings are currently unavailable</string>
<string name="can_deleted">CAN deleted</string>
<string name="pin_deleted">PIN 1 deleted</string>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="helper">
<item name="android:fontFamily">sans-serif</item>
<item name="android:textSize">@dimen/helper_text</item>
</style>
</resources>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MobileAuthApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.MobileAuthApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/blue_500</item>
<item name="colorPrimaryVariant">@color/blue_700</item>

View File

@ -9,7 +9,7 @@ buildscript {
kotlin_version = "1.4.30"
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath 'com.android.tools.build:gradle:7.0.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -2,16 +2,24 @@
This is a proof-of-concept project for creating an Android app for authenticating yourself using an NFC-enabled Estonian ID card. This project will be created for the University of Tartu course "Software project".
[Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision)
### Requirements to use the application
* The smartphone's operating system must be Android 8.0 or newer
* The smartphone must support NFC technology and it must be enabled
* The user must have an Estonian ID card with NFC capability (issued since 2018)
[Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes)
### Installing the application on the phone
The first option is to open the MobileAuthApp folder of the project on the Android Studio and use the smartphone instead of an emulator (the application does not work with emulators because real ID card has to be scanned, which an emulator can not do) to run the application. This way the application gets installed on the phone automatically.
[Project tasks](https://tvp-mobile-authentication.atlassian.net/jira/software/projects/MOB/boards/1/backlog) (Ask Tanel for JIRA permissions if needed).
More information about using real devices with Android studio: https://developer.android.com/studio/run/device
[Project plan](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-plan)
The second and more reliable option is to get the .apk that is generated under the Artifacts of GitHub Actions when the project is built. Download the .apk file and move it to the smartphone and install it (phone permissions might have to be changed because it is not installed through Google Play). After the application has been installed it should open as any other application.
[Use Cases](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Cases) *updated 08.10*
More info about installing third party applications on the Android phones: https://www.androidauthority.com/how-to-install-apks-31494/
[User stories](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/User-stories)
**NB! Before using the application make sure that the NFC is enabled on the phone, otherwise information can not be read from the ID card.**
[Use Case Tests](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Case-Tests)
### Testing the application
The project comes with a test mobile application and a test web application that can be used to try the MobileAuthApp authentication feature even if you don't have any web applications or mobile applications that require user authentication. Both projects come with a README file that help with a setup.
The mobile authentication application, when launched by the user not a website or some other application, can also read card holder's information, which can be used to verify whether the application reads the information from the ID card correctly.
### See the [Wiki](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki) for pages relevant for the "Software project" subject

16
TestMobileApp/.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
*.iml
.gradle
.idea
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

10
TestMobileApp/README.md Normal file
View File

@ -0,0 +1,10 @@
# TestMobileApp overview
### The purpose
The TestMobileApp was created in order to demonstrate how a different application on the Android smartphone could use the MobileAuthApp for user authentication purposes.
### Installing the application
The application installation process is the same as with the MobileAuthApp. Check the guide in the project's [main readme file](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC#installing-the-application-on-the-phone).
### Using the application
In order to use this application a backend server must be running that can issue challenges and verify the token created by the MobileAuthApp.
Use demoBackend application that is included in the project. Follow the demoBackend setup guide and once you have a backend running take the https address of the backend
and add it in the TestMobileApp's MainActivty.kt file as the new value for the constant variable BASE_URL (this is easly noticeable in the class as it is pointed out with a comment).
Now the app can be used.

1
TestMobileApp/app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,47 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 31
defaultConfig {
applicationId "com.example.testmobileapp"
minSdk 26
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'com.koushikdutta.ion:ion:3.1.0'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

21
TestMobileApp/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.example.testmobileapp
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.testmobileapp", appContext.packageName)
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.testmobileapp">
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestMobileApp">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,94 @@
package com.example.testmobileapp
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.example.testmobileapp.databinding.ActivityMainBinding
import org.json.JSONObject
/**
* Base url where the requests should be made. Add yours here. It must use https.
*/
private const val BASE_URL = "https://a0fe-2001-7d0-88ab-b880-7571-cba0-5db2-11b7.ngrok.io"
private const val AUTH_URL = "$BASE_URL/auth/login"
private const val CHALLENGE_URL = "$BASE_URL/auth/challenge"
/**
* Test mobile app to demonstrate how other applications could potentially use MobileAuthApp.
* Single purpose app that launches the MobileAuthApp and gets the response back (JWT).
* Only for demo purposes.
*/
class MainActivity : AppCompatActivity() {
private lateinit var authLauncher: ActivityResultLauncher<Intent>
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
if (response.resultCode == Activity.RESULT_OK) {
binding.loginTextView.text = getString(R.string.auth_success)
// Logs are used to show what information can be retrieved from the mobileauthapp.
Log.i("getResult", response.data?.getStringExtra("idCode").toString())
Log.i("getResult", response.data?.getStringExtra("name").toString())
Log.i("getResult", response.data?.getStringExtra("authority").toString())
var user = ""
try {
user = response.data?.getStringExtra("name").toString()
} catch (e: Exception) {
Log.i("getResult", "unable to retrieve name")
}
showResult(user)
}
if (response.resultCode == Activity.RESULT_CANCELED) {
binding.loginTextView.text = getString(R.string.auth_failure)
}
}
showLogin()
binding.loginOptionNfcButton.setOnClickListener {
launchAuth()
}
}
/**
* Method that creates an intent to launch the MobileAuthApp
*/
private fun launchAuth() {
val launchIntent = Intent()
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
launchIntent.putExtra("action", "auth")
launchIntent.putExtra("challenge", CHALLENGE_URL)
launchIntent.putExtra("originUrl", BASE_URL)
launchIntent.putExtra("authUrl", AUTH_URL)
launchIntent.putExtra("headers","${(0..100000).random()}")
launchIntent.putExtra("mobile", true)
authLauncher.launch(launchIntent)
}
private fun showLogin() {
binding.loginOptions.visibility = View.VISIBLE
}
private fun showResult(user: String) {
binding.loginOptions.visibility = View.GONE
binding.resultLayout.visibility = View.VISIBLE
binding.resultObject.text = getString(R.string.hello, user)
binding.buttonForget.setOnClickListener {
binding.loginTextView.text = getString(R.string.login_text)
binding.resultObject.text = ""
binding.resultLayout.visibility = View.GONE
binding.loginOptions.visibility = View.VISIBLE
}
}
}

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/login_text_view"
android:text="@string/login_text"
android:textSize="20sp"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout
android:id="@+id/login_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_text_view"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone">
<TextView
android:id="@+id/choose_method_text_view"
android:text="@string/choose_login_method"
android:textSize="16sp"
android:layout_marginVertical="6dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/login_option_nfc_button"
android:text="@string/method_nfc"
android:textSize="14sp"
android:layout_marginVertical="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/login_text_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone">
<TextView
android:id="@+id/result_object"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:textSize="18sp"/>
<Button
android:id="@+id/button_forget"
android:text="@string/forget_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TestMobileApp</string>
<string name="login_text">Login</string>
<string name="choose_login_method">Choose login method</string>
<string name="method_nfc">NFC auth</string>
<string name="auth_success">Logged in</string>
<string name="auth_failure">Response failed</string>
<string name="forget_button">Forget</string>
<string name="hello">Hello, %s!</string>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TestMobileApp</string>
<string name="login_text">Logi sisse</string>
<string name="choose_login_method">Vali sobiv meetod</string>
<string name="method_nfc">NFC auth</string>
<string name="auth_success">Sisse logimine õnnestus</string>
<string name="auth_failure">Vastust ei õnnestunud kätte saada</string>
<string name="forget_button">Unusta</string>
<string name="hello">Tere, %s!</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TestMobileApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/red_200</item>
<item name="colorPrimaryVariant">@color/red_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/amber_200</item>
<item name="colorSecondaryVariant">@color/amber_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- New colors -->
<color name="red_200">#ef9a9a</color>
<color name="red_500">#f44336</color>
<color name="red_700">#d32f2f</color>
<color name="amber_200">#ffe082</color>
<color name="amber_700">#ffa000</color>
</resources>

View File

@ -0,0 +1,10 @@
<resources>
<string name="app_name">TestMobileApp</string>
<string name="login_text">Login</string>
<string name="choose_login_method">Choose login method</string>
<string name="method_nfc">NFC auth</string>
<string name="auth_success">Logged in</string>
<string name="auth_failure">Response failed</string>
<string name="forget_button">Forget</string>
<string name="hello">Hello, %s!</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TestMobileApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/red_500</item>
<item name="colorPrimaryVariant">@color/red_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/amber_200</item>
<item name="colorSecondaryVariant">@color/amber_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -0,0 +1,17 @@
package com.example.testmobileapp
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -0,0 +1,19 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Tue Oct 26 10:33:23 EEST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

185
TestMobileApp/gradlew vendored Normal file
View File

@ -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
;;
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
TestMobileApp/gradlew.bat vendored Normal file
View File

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

View File

@ -0,0 +1,10 @@
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
rootProject.name = "TestMobileApp"
include ':app'

36
demoBackend/.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### web-eid.js ###
src/demo-website/src/web-eid.js

View File

@ -0,0 +1,118 @@
/*
* Copyright 2007-present 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.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if (mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if (mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if (!outputFile.getParentFile().exists()) {
if (!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}

Binary file not shown.

View File

@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

39
demoBackend/README.md Normal file
View File

@ -0,0 +1,39 @@
# Demo backend + website for mobile authentication project.
## How to run.
### 1. Clone GIT repository
### 2. Setup HTTPS
Since Web eID only works over HTTPS connection, you'll need to serve the backend and website with an HTTPS certificate. A suitable tool for that is ngrok (https://ngrok.com/).
To use ngrok, download it and then run the command (may need administrator rights)
```ngrok http 8080```
and you should see something like this:
```
ngrok by @inconshreveable (Ctrl+C to quit)
Session Status online
Account TanelOrumaa (Plan: Free)
Version 2.3.40
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://somethinghere.ngrok.io -> http://localhost:8080
Forwarding https://somethinghere.ngrok.io -> http://localhost:8080
Connections ttl opn rt1 rt5 p50 p90
1508 0 0.00 0.00 2.31 75.59
HTTP Requests
-------------
```
Copy the second forwarding link (the one with https) and put it in ```com.tarkvaratehnika.demobackend.config.ApplicationConfiguration.kt``` as ```val WEBSITE_ORIGIN_URL = "https://yourlinkhere.com"```
### 3. Run the project
Use your favourite IDE or just run it via commandline with ```./mvnw spring-boot:run```
On your browser (Android to test out from Android device or desktop to try out ID-card reader or QR-code capability) navigate to the url you copied earlier and you should see the website landing page. If you have the mobile authentication app installed, you should be able to log into the website with your Estonian ID-card.
## Credits...
...go out to creators of https://github.com/web-eid/web-eid-spring-boot-example. That example project was used in some parts as an example (files where inspiration was taken are correctly annotated with the appropriate license text).

310
demoBackend/mvnw vendored Normal file
View File

@ -0,0 +1,310 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
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
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
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
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

182
demoBackend/mvnw.cmd vendored Normal file
View File

@ -0,0 +1,182 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. 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,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

210
demoBackend/pom.xml Normal file
View File

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tarkvaratehnika</groupId>
<artifactId>demoBackend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demoBackend</name>
<description>demoBackend</description>
<packaging>jar</packaging>
<properties>
<java.version>11</java.version>
<kotlin.version>1.5.31</kotlin.version>
<caffeine.version>2.8.5</caffeine.version>
<javaxcache.version>1.1.1</javaxcache.version>
<node.version>v16.13.0</node.version>
<npm.version>8.1.4</npm.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
</dependency>
<dependency>
<groupId>org.webeid.security</groupId>
<artifactId>authtoken-validation</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>${javaxcache.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>jcache</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>gitlab</id>
<url>https://gitlab.com/api/v4/projects/19948337/packages/maven</url>
</repository>
</repositories>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
<!-- Plugin to install node and npm and then build the vue project -->
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.0</version>
<executions>
<execution>
<id>Install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
<npmVersion>${npm.version}</npmVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm build</id>
<goals>
<goal>npm</goal>
</goals>
<phase>process-resources</phase>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
<workingDirectory>src/demo-website</workingDirectory>
<!-- <installDirectory>src/demo-website/dist</installDirectory>-->
</configuration>
</plugin>
<!-- Plugin to copy built vue project from src/frontend/dist to target/classes/static -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>Copy web-eid.js file to Vue root folder.</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>src/demo-website/src</outputDirectory>
<resources>
<resource>
<directory>src/demo-website/node_modules/@web-eid/web-eid-library/dist/es</directory>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>Copy Vue frontend into Spring Boot target static folder</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes/static</outputDirectory>
<resources>
<resource>
<directory>src/demo-website/dist</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

23
demoBackend/src/demo-website/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

Binary file not shown.

View File

@ -0,0 +1,24 @@
# demo-website
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -0,0 +1 @@
#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50}#nav{padding:30px}#nav a{font-weight:700;color:#2c3e50}#nav a.router-link-exact-active{color:#42b983}.container>div[data-v-2dcb24ca]{margin-top:2vh}.loginButton[data-v-2dcb24ca]{height:4vh;width:20vh;line-height:3vh}.loginButton>p[data-v-2dcb24ca]{font-size:3vh;text-align:center}#canvas[data-v-2dcb24ca]{height:30vh;width:30vh}nav[data-v-21165a6a]{height:5vh}div[data-v-cd8fea1a]{margin-top:2vh}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>demo-website</title><link href="/css/app.eb039c1f.css" rel="preload" as="style"><link href="/css/chunk-vendors.a251e031.css" rel="preload" as="style"><link href="/js/app.c2a68e49.js" rel="preload" as="script"><link href="/js/chunk-vendors.22b03028.js" rel="preload" as="script"><link href="/css/chunk-vendors.a251e031.css" rel="stylesheet"><link href="/css/app.eb039c1f.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but demo-website doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.22b03028.js"></script><script src="/js/app.c2a68e49.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More