mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2025-08-30 15:20:58 +03:00
Compare commits
52 Commits
iteration-
...
Tests_old
Author | SHA1 | Date | |
---|---|---|---|
|
1cd86cc4f8 | ||
|
634672db0f | ||
|
be5c0568f8 | ||
|
b8c2da95a2 | ||
|
624ebf3999 | ||
|
7dde3239a0 | ||
|
0dfeb798e3 | ||
|
371a871f87 | ||
|
8793ed9179 | ||
|
0c205eea8d | ||
|
8fe7aed941 | ||
|
57de1bf979 | ||
|
5c1f141405 | ||
|
a61ea0b6cc | ||
|
8d5a55c00e | ||
|
bf87eb1c07 | ||
|
7e14bc289e | ||
|
f274b48d68 | ||
|
515eea14bb | ||
|
e4a06b4fc9 | ||
|
24980b3253 | ||
|
6ddfe9af46 | ||
|
60207319b7 | ||
|
67ba0ed764 | ||
|
da2ba0b0da | ||
|
73b94adcd3 | ||
|
339fa0a378 | ||
|
e5300dfa5e | ||
|
d4c2a11521 | ||
|
0e15bede78 | ||
|
09c4fa6be3 | ||
|
63bc89b0e4 | ||
|
152fd16162 | ||
|
716b983389 | ||
|
94fad95364 | ||
|
c33fba1a14 | ||
|
825335ea5f | ||
|
0f6f31c995 | ||
|
762a8c8cc2 | ||
|
1138abcb11 | ||
|
f085076631 | ||
|
edc444c027 | ||
|
df5febabb7 | ||
|
1b9a59d4eb | ||
|
bbd5039a0b | ||
|
2c5430977d | ||
|
68a7db2e77 | ||
|
a4caf24a35 | ||
|
5b70a8f997 | ||
|
168c9be010 | ||
|
636beeb7f3 | ||
|
73efb00368 |
@@ -44,7 +44,9 @@ dependencies {
|
|||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
|
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
debugImplementation 'androidx.fragment:fragment-testing:1.4.0'
|
||||||
|
|
||||||
//To use activityViewModels
|
//To use activityViewModels
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
@@ -68,8 +70,4 @@ dependencies {
|
|||||||
'io.jsonwebtoken:jjwt-gson:0.11.2'
|
'io.jsonwebtoken:jjwt-gson:0.11.2'
|
||||||
|
|
||||||
implementation 'com.koushikdutta.ion:ion:3.1.0'
|
implementation 'com.koushikdutta.ion:ion:3.1.0'
|
||||||
|
|
||||||
// Retrofit + Moshi Converter
|
|
||||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
|
||||||
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
|
|
||||||
}
|
}
|
@@ -0,0 +1,62 @@
|
|||||||
|
package com.tarkvaraprojekt.mobileauthapp
|
||||||
|
|
||||||
|
//import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.IdlingPolicies
|
||||||
|
import androidx.test.espresso.NoMatchingViewException
|
||||||
|
import androidx.test.espresso.action.ViewActions.*
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
|
||||||
|
import org.junit.*
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class UC4Test {
|
||||||
|
@get:Rule
|
||||||
|
var activityActivityTestRule: ActivityTestRule<MainActivity> = ActivityTestRule(
|
||||||
|
MainActivity::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
IdlingPolicies.setMasterPolicyTimeout(3, TimeUnit.SECONDS)
|
||||||
|
IdlingPolicies.setIdlingResourceTimeout(3, TimeUnit.SECONDS)
|
||||||
|
activityActivityTestRule.activity
|
||||||
|
.supportFragmentManager.beginTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateToCANView() {
|
||||||
|
onView(withId(R.id.menu_settings_option)).perform(click())
|
||||||
|
try {
|
||||||
|
// Delete existing CAN
|
||||||
|
onView(withText(R.string.can_delete)).perform(click())
|
||||||
|
} catch (ignore: NoMatchingViewException) {}
|
||||||
|
|
||||||
|
onView(withId(R.id.can_menu_action)).perform(click())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validCAN() {
|
||||||
|
navigateToCANView()
|
||||||
|
onView(withText(R.string.can_helper_text)).check(matches(isDisplayed()))
|
||||||
|
onView(supportsInputMethods()).perform(typeText("123456"))
|
||||||
|
onView(withText(R.string.can_delete)).perform(closeSoftKeyboard())
|
||||||
|
|
||||||
|
onView(withText(R.string.can_status_saved)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun invalidCAN() {
|
||||||
|
navigateToCANView()
|
||||||
|
onView(supportsInputMethods()).perform(typeText("12345"))
|
||||||
|
onView(withText(R.string.can_helper_text)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,10 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp
|
package com.tarkvaraprojekt.mobileauthapp
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.nfc.NfcAdapter
|
import android.nfc.NfcAdapter
|
||||||
import android.nfc.tech.IsoDep
|
import android.nfc.tech.IsoDep
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -17,11 +14,14 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||||
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
|
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
|
import java.io.IOException
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,24 +77,19 @@ class AuthFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
||||||
|
if (args.reading) {
|
||||||
adapter.enableReaderMode(activity, { tag ->
|
adapter.enableReaderMode(activity, { tag ->
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
requireActivity().runOnUiThread {
|
requireActivity().runOnUiThread {
|
||||||
binding!!.timeCounter.text = getString(R.string.card_detected)
|
binding!!.timeCounter.text = getString(R.string.card_detected)
|
||||||
}
|
}
|
||||||
|
var msgCode = 0
|
||||||
|
|
||||||
val card = IsoDep.get(tag)
|
val card = IsoDep.get(tag)
|
||||||
card.timeout = 32768
|
card.timeout = 32768
|
||||||
card.use {
|
card.use {
|
||||||
try {
|
try {
|
||||||
val comms = Comms(it, viewModel.userCan)
|
val comms = Comms(it, viewModel.userCan)
|
||||||
if (args.auth) {
|
|
||||||
val jws = Authenticator(comms).authenticate(
|
|
||||||
intentParameters.challenge,
|
|
||||||
intentParameters.authUrl,
|
|
||||||
viewModel.userPin
|
|
||||||
)
|
|
||||||
intentParameters.setToken(jws)
|
|
||||||
} else {
|
|
||||||
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
|
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
|
||||||
viewModel.setUserFirstName(response[1])
|
viewModel.setUserFirstName(response[1])
|
||||||
viewModel.setUserLastName(response[0])
|
viewModel.setUserLastName(response[0])
|
||||||
@@ -102,24 +97,45 @@ class AuthFragment : Fragment() {
|
|||||||
viewModel.setGender(response[3])
|
viewModel.setGender(response[3])
|
||||||
viewModel.setCitizenship(response[4])
|
viewModel.setCitizenship(response[4])
|
||||||
viewModel.setExpiration(response[5])
|
viewModel.setExpiration(response[5])
|
||||||
}
|
|
||||||
requireActivity().runOnUiThread {
|
requireActivity().runOnUiThread {
|
||||||
binding!!.timeCounter.text = getString(R.string.data_read)
|
binding!!.timeCounter.text = getString(R.string.data_read)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
requireActivity().runOnUiThread {
|
} catch (e: android.nfc.TagLostException) {
|
||||||
binding!!.timeCounter.text = getString(R.string.no_success)
|
msgCode = R.string.tag_lost
|
||||||
}
|
} catch (e: InvalidCANException) {
|
||||||
|
msgCode = R.string.invalid_can
|
||||||
// If the CAN is wrong we will also delete the saved CAN so that the user won't use it again.
|
// If the CAN is wrong we will also delete the saved CAN so that the user won't use it again.
|
||||||
viewModel.deleteCan(requireContext())
|
viewModel.deleteCan(requireContext())
|
||||||
// Gives user some time to read the error message
|
} catch (e: AuthAppException) {
|
||||||
Thread.sleep(1000)
|
msgCode = when (e.code) {
|
||||||
goToTheStart()
|
448 -> R.string.err_bad_data
|
||||||
|
500 -> R.string.err_internal
|
||||||
|
else -> R.string.err_unknown
|
||||||
|
}
|
||||||
|
} catch (e: GeneralSecurityException) {
|
||||||
|
msgCode = R.string.err_internal
|
||||||
|
} catch (e: IOException) {
|
||||||
|
msgCode = R.string.err_reading_card
|
||||||
|
} catch (e: Exception) {
|
||||||
|
msgCode = R.string.err_unknown
|
||||||
} finally {
|
} finally {
|
||||||
adapter.disableReaderMode(activity)
|
adapter.disableReaderMode(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msgCode != 0) {
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
binding!!.timeCounter.text = getString(msgCode)
|
||||||
|
}
|
||||||
|
// Gives user some time to read the error message
|
||||||
|
Thread.sleep(1000)
|
||||||
|
goToTheStart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
||||||
|
} else { //We want to create a JWT instead of reading the info from the card.
|
||||||
|
goToNextFragment()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goToNextFragment() {
|
private fun goToNextFragment() {
|
||||||
|
@@ -3,17 +3,22 @@ package com.tarkvaraprojekt.mobileauthapp
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
|
import org.w3c.dom.Text
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved
|
* Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved
|
||||||
@@ -24,12 +29,11 @@ class CanFragment : Fragment() {
|
|||||||
|
|
||||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentCanBinding? = null
|
private var _binding: FragmentCanBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
// Navigation arguments:
|
// Navigation arguments:
|
||||||
// saving = true means that we are navigating here from the settings menu and must return to the settings menu.
|
// saving = true means that we are navigating here from the settings menu and must return to the settings menu.
|
||||||
// reading = true means that we are only reading the information from the ID card that does not need PIN 1,
|
|
||||||
// this information is passed on to the next PinFragment.
|
|
||||||
private val args: CanFragmentArgs by navArgs()
|
private val args: CanFragmentArgs by navArgs()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -37,20 +41,17 @@ class CanFragment : Fragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentCanBinding.inflate(inflater, container, false)
|
_binding = FragmentCanBinding.inflate(inflater, container, false)
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
checkIfSkip()
|
checkIfSkip()
|
||||||
// If the user arrives from the settings menu then the button should say
|
binding.canTextField.editText?.addTextChangedListener {
|
||||||
// save instead of continue.
|
checkEnteredCan()
|
||||||
if (args.saving) {
|
|
||||||
binding!!.nextButton.text = getString(R.string.save_text)
|
|
||||||
}
|
}
|
||||||
binding!!.nextButton.setOnClickListener { checkEnteredCan() }
|
binding.buttonCancel.setOnClickListener { goToTheStart() }
|
||||||
binding!!.cancelButton.setOnClickListener { goToTheStart() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,77 +68,66 @@ class CanFragment : Fragment() {
|
|||||||
* Takes user to the next fragment, which is PinFragment.
|
* Takes user to the next fragment, which is PinFragment.
|
||||||
*/
|
*/
|
||||||
private fun goToTheNextFragment() {
|
private fun goToTheNextFragment() {
|
||||||
val action = CanFragmentDirections.actionCanFragmentToPinFragment(reading = args.reading, auth = args.auth, mobile = args.mobile)
|
val action = CanFragmentDirections.actionCanFragmentToPinFragment(auth = args.auth, mobile = args.mobile)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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!!.canEditText.editText?.text.toString()
|
|
||||||
if (enteredCan.length == 6) {
|
|
||||||
viewModel.setUserCan(enteredCan)
|
|
||||||
if (args.saving) {
|
|
||||||
viewModel.storeCan(requireContext())
|
|
||||||
goToTheStart()
|
|
||||||
} else {
|
|
||||||
val storeCanQuestion = getDialog()
|
|
||||||
storeCanQuestion?.show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.length_can), Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a dialog that asks the user whether the entered CAN should be saved
|
|
||||||
* on the device or not.
|
|
||||||
*/
|
|
||||||
private fun getDialog(): AlertDialog? {
|
|
||||||
return activity?.let { frag ->
|
|
||||||
val builder = AlertDialog.Builder(frag)
|
|
||||||
builder.apply {
|
|
||||||
// If response is positive then save the CAN on the device.
|
|
||||||
setPositiveButton(R.string.save_text) { _, _ ->
|
|
||||||
viewModel.storeCan(
|
|
||||||
requireContext()
|
|
||||||
)
|
|
||||||
goToTheNextFragment()
|
|
||||||
}
|
|
||||||
setNegativeButton(R.string.deny_text) { _, _ ->
|
|
||||||
goToTheNextFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.setMessage(R.string.can_save_request)
|
|
||||||
builder.setTitle(R.string.save_can_title)
|
|
||||||
builder.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates the user back to the start depending on where the user arrived.
|
* 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
|
* If the user arrived from the settings menu then the start is the settings menu
|
||||||
* not the HomeFragment.
|
* not the HomeFragment.
|
||||||
*/
|
*/
|
||||||
private fun goToTheStart() {
|
private fun goToTheStart() {
|
||||||
// TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment.
|
|
||||||
if (args.saving) {
|
if (args.saving) {
|
||||||
|
if (args.fromhome) {
|
||||||
|
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
||||||
|
} else {
|
||||||
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
|
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
|
||||||
} else if (args.auth) {
|
}
|
||||||
|
} else if (args.auth || args.mobile) {
|
||||||
|
if (args.mobile) {
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
|
} else {
|
||||||
|
requireActivity().finishAndRemoveTask()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,15 +1,26 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp
|
package com.tarkvaraprojekt.mobileauthapp
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.nfc.NfcAdapter
|
||||||
|
import android.nfc.TagLostException
|
||||||
|
import android.nfc.tech.IsoDep
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
@@ -28,75 +39,129 @@ class HomeFragment : Fragment() {
|
|||||||
|
|
||||||
private val intentParams: ParametersViewModel by activityViewModels()
|
private val intentParams: ParametersViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentHomeBinding? = null
|
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(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
// Making settings menu active again
|
// Making settings menu active again
|
||||||
(activity as MainActivity).menuAvailable = true
|
(activity as MainActivity).menuAvailable = true
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
initialChecks()
|
initialChecks()
|
||||||
var auth = false
|
|
||||||
if (requireActivity().intent.data?.getQueryParameter("action") != null) {
|
if (requireActivity().intent.data?.getQueryParameter("action") != null) {
|
||||||
// Currently we only support authentication not signing.
|
// Currently we only support authentication not signing.
|
||||||
auth = true
|
auth = true
|
||||||
}
|
}
|
||||||
val mobile = requireActivity().intent.getBooleanExtra("mobile", false)
|
val mobile = requireActivity().intent.getBooleanExtra("mobile", false)
|
||||||
if (auth || mobile) {
|
if (auth || mobile) {
|
||||||
try {
|
startAuthentication(mobile)
|
||||||
if (mobile) {
|
} else {
|
||||||
// We use !! because we want an exception when something is not right.
|
receiver = object : BroadcastReceiver() {
|
||||||
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
override fun onReceive(p0: Context?, p1: Intent?) {
|
||||||
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
|
updateAction(canSaved)
|
||||||
} else { //Website
|
|
||||||
// Currently the test website won't send the authUrl parameter
|
|
||||||
//Log.i("intentDebugging", requireActivity().intent.data.toString())
|
|
||||||
intentParams.setChallenge(requireActivity().intent.data!!.getQueryParameter("challenge")!!)
|
|
||||||
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
// There was a problem with parameters, which means that authentication is not possible.
|
|
||||||
val resultIntent = Intent()
|
|
||||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
}
|
||||||
goToTheNextFragment(true, mobile)
|
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
|
||||||
|
requireActivity().registerReceiver(receiver, filter)
|
||||||
|
updateAction(canSaved)
|
||||||
}
|
}
|
||||||
binding!!.beginButton.setOnClickListener { goToTheNextFragment() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method where all the initial checks that should be done before any user input is accepted should be added.
|
|
||||||
*/
|
|
||||||
private fun initialChecks() {
|
|
||||||
viewModel.checkCan(requireContext())
|
|
||||||
viewModel.checkPin(requireContext())
|
|
||||||
displayStates()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
|
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
|
||||||
*/
|
*/
|
||||||
private fun goToTheNextFragment(auth: Boolean = false, mobile: Boolean = false) {
|
private fun goToTheNextFragment(mobile: Boolean = false) {
|
||||||
// Making settings menu inactive
|
|
||||||
(activity as MainActivity).menuAvailable = false
|
(activity as MainActivity).menuAvailable = false
|
||||||
// Currently saving is true because the application is not yet integrated with
|
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
|
||||||
// other applications or websites.
|
|
||||||
// TODO: Check the navigation action default values. Not everything has to be declared explicitly.
|
|
||||||
if (auth) {
|
|
||||||
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(reading = false, auth = true, mobile = mobile)
|
|
||||||
findNavController().navigate(action)
|
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")!!)
|
||||||
|
} 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")!!)
|
||||||
|
}
|
||||||
|
goToTheNextFragment(mobile)
|
||||||
|
} 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 {
|
} else {
|
||||||
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(reading = true, auth = false, mobile = mobile)
|
message.setMessage(getString(R.string.problem_other))
|
||||||
findNavController().navigate(action)
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,33 +176,117 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the state of the CAN, saved or not saved. Updates the text and logo.
|
* Method where all the initial checks that should be completed before any user input is accepted should be conducted.
|
||||||
*/
|
*/
|
||||||
private fun canState() {
|
private fun initialChecks() {
|
||||||
if (viewModel.userCan.length == 6) {
|
viewModel.checkCan(requireContext())
|
||||||
binding!!.canStatusText.text = getString(R.string.can_status_saved)
|
viewModel.checkPin(requireContext())
|
||||||
binding!!.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
|
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 {
|
} else {
|
||||||
binding!!.canStatusText.text = getString(R.string.can_status_negative)
|
binding.detectionActionText.text = getString(R.string.action_detect_unavailable)
|
||||||
binding!!.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the state of the PIN 1, saved or not saved. Updates the text and logo.
|
* Resets the error message and allows the user to try again
|
||||||
*/
|
*/
|
||||||
private fun pinState() {
|
private fun reset() {
|
||||||
if (viewModel.userPin.length in 4..12) {
|
binding.homeActionButton.text = getString(R.string.try_again_text)
|
||||||
binding!!.pinStatusText.text = getString(R.string.pin_status_saved)
|
binding.homeActionButton.setOnClickListener {
|
||||||
binding!!.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
|
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 {
|
} else {
|
||||||
binding!!.pinStatusText.text = getString(R.string.pin_status_negative)
|
adapter.enableReaderMode(activity, { tag ->
|
||||||
binding!!.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
binding = null
|
if (receiver != null) {
|
||||||
|
requireActivity().unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -20,6 +20,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
|
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
|
||||||
var menuAvailable: Boolean = true
|
var menuAvailable: Boolean = true
|
||||||
|
|
||||||
|
var inMenu: Boolean = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
@@ -40,9 +42,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
R.id.menu_settings_option -> {
|
R.id.menu_settings_option -> {
|
||||||
if (menuAvailable) {
|
if (menuAvailable) {
|
||||||
navigationController.navigate(R.id.action_homeFragment_to_settingsFragment)
|
navigationController.navigate(R.id.action_homeFragment_to_settingsFragment)
|
||||||
|
menuAvailable = false
|
||||||
|
inMenu = true
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, getString(R.string.unavailable), Toast.LENGTH_SHORT).show()
|
if (!inMenu) {
|
||||||
|
Toast.makeText(this, getString(R.string.menu_unavailable_message), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,9 @@ package com.tarkvaraprojekt.mobileauthapp.NFC;
|
|||||||
import android.nfc.tech.IsoDep;
|
import android.nfc.tech.IsoDep;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException;
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.BlockCipher;
|
import org.bouncycastle.crypto.BlockCipher;
|
||||||
import org.bouncycastle.crypto.engines.AESEngine;
|
import org.bouncycastle.crypto.engines.AESEngine;
|
||||||
import org.bouncycastle.crypto.macs.CMac;
|
import org.bouncycastle.crypto.macs.CMac;
|
||||||
@@ -21,7 +24,6 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
@@ -31,43 +33,47 @@ import javax.crypto.spec.IvParameterSpec;
|
|||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
public class Comms {
|
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[] selectMaster = Hex.decode("00a4040c10a000000077010800070000fe00000100");
|
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[] MSESetAT = Hex.decode("0022c1a40f800a04007f0007020204020483010200");
|
private static final byte[] GAGetNonce = { // general authenticate: get nonce
|
||||||
|
16, -122, 0, 0, 2, 124, 0, 0
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAGetNonce = Hex.decode("10860000027c0000");
|
private static final byte[] GAMapNonceIncomplete = {
|
||||||
|
16, -122, 0, 0, 69, 124, 67, -127, 65
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAMapNonceIncomplete = Hex.decode("10860000457c438141");
|
private static final byte[] GAKeyAgreementIncomplete = {
|
||||||
|
16, -122, 0, 0, 69, 124, 67, -125, 65
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAKeyAgreementIncomplete = Hex.decode("10860000457c438341");
|
private static final byte[] GAMutualAuthenticationIncomplete = {
|
||||||
|
0, -122, 0, 0, 12, 124, 10, -123, 8
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAMutualAuthenticationIncomplete = Hex.decode("008600000c7c0a8508");
|
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[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641");
|
private static final byte[] masterSec = {
|
||||||
|
12, -92, 4, 12, 45, -121, 33, 1
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] selectFile = Hex.decode("0ca4010c1d871101");
|
private static final byte[] personal = { // select personal data DF
|
||||||
|
12, -92, 1, 12, 29, -121, 17, 1
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] readFile = Hex.decode("0cb000000d970100");
|
private static final byte[] read = { // read binary
|
||||||
|
12, -80, 0, 0, 13, -105, 1, 0
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101");
|
private IsoDep idCard;
|
||||||
|
|
||||||
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[] keyEnc;
|
||||||
private final byte[] keyMAC;
|
private final byte[] keyMAC;
|
||||||
private byte ssc; // Send sequence counter.
|
private byte ssc; // Send sequence counter.
|
||||||
@@ -81,12 +87,21 @@ public class Comms {
|
|||||||
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||||
|
|
||||||
idCard.connect();
|
idCard.connect();
|
||||||
|
|
||||||
this.idCard = idCard;
|
this.idCard = idCard;
|
||||||
byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
byte[][] keys = PACE(CAN);
|
||||||
|
Log.i("Pace duration", String.valueOf(System.currentTimeMillis() - start));
|
||||||
|
|
||||||
keyEnc = keys[0];
|
keyEnc = keys[0];
|
||||||
keyMAC = keys[1];
|
keyMAC = keys[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getAuthenticationCertificate() {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the message authentication code
|
* Calculates the message authentication code
|
||||||
*
|
*
|
||||||
@@ -139,51 +154,40 @@ public class Comms {
|
|||||||
* @param CAN the card access number provided by the user
|
* @param CAN the card access number provided by the user
|
||||||
* @return the decrypted nonce
|
* @return the decrypted nonce
|
||||||
*/
|
*/
|
||||||
private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||||
byte[] decryptionKey = createKey(CAN, (byte) 3);
|
byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3);
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||||
return cipher.doFinal(encryptedNonce);
|
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
|
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
|
||||||
*
|
*
|
||||||
* @param CAN the card access number
|
* @param CAN the card access number
|
||||||
*/
|
*/
|
||||||
private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
||||||
|
|
||||||
// select the IAS-ECC application on the chip
|
// select the ECC applet on the chip
|
||||||
getResponse(selectMaster, "Select the master application");
|
byte[] response = idCard.transceive(master);
|
||||||
|
Log.i("Select applet", Hex.toHexString(response));
|
||||||
|
|
||||||
// initiate PACE
|
// initiate PACE
|
||||||
getResponse(MSESetAT, "Set authentication template");
|
response = idCard.transceive(MSESetAT);
|
||||||
|
Log.i("Authentication template", Hex.toHexString(response));
|
||||||
|
|
||||||
// get nonce
|
// get nonce
|
||||||
byte[] response = getResponse(GAGetNonce, "Get nonce");
|
response = idCard.transceive(GAGetNonce);
|
||||||
|
Log.i("Get nonce", Hex.toHexString(response));
|
||||||
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
|
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
|
||||||
|
|
||||||
// generate an EC keypair and exchange public keys with the chip
|
// generate an EC keypair and exchange public keys with the chip
|
||||||
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
|
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
|
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();
|
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
|
||||||
response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce");
|
byte[] APDU = createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i("Map nonce", Hex.toHexString(response));
|
||||||
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
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
|
// calculate the new base point, use it to generate a new keypair, and exchange public keys
|
||||||
@@ -191,41 +195,35 @@ public class Comms {
|
|||||||
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
|
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
|
||||||
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
|
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
|
||||||
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
|
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
|
||||||
response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement");
|
APDU = createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i("Key agreement", Hex.toHexString(response));
|
||||||
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||||
|
|
||||||
// generate the session keys and exchange MACs to verify them
|
// generate the session keys and exchange MACs to verify them
|
||||||
byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
|
sharedSecret = cardPublicKey.multiply(privateKey).normalize();
|
||||||
byte[] keyEnc = createKey(secret, (byte) 1);
|
byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded();
|
||||||
byte[] keyMAC = createKey(secret, (byte) 2);
|
byte[] keyEnc = createKey(encodedSecret, (byte) 1);
|
||||||
byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
|
byte[] keyMAC = createKey(encodedSecret, (byte) 2);
|
||||||
response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
|
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));
|
||||||
|
|
||||||
// verify chip's MAC and return session keys
|
// if the chip-side verification fails, crash and burn
|
||||||
MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
|
if (response.length == 2) throw new InvalidCANException();
|
||||||
|
|
||||||
|
// otherwise verify chip's MAC and return session keys
|
||||||
|
APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65);
|
||||||
|
MAC = getMAC(APDU, keyMAC);
|
||||||
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
|
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
|
||||||
throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen.
|
throw new AuthAppException("Could not verify chip's MAC.", 448); // Should never happen.
|
||||||
}
|
}
|
||||||
return new byte[][]{keyEnc, keyMAC};
|
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
|
* Encrypts or decrypts the APDU data
|
||||||
*
|
*
|
||||||
@@ -260,182 +258,74 @@ public class Comms {
|
|||||||
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
|
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
|
||||||
macData[15] = ssc; // first block contains the ssc
|
macData[15] = ssc; // first block contains the ssc
|
||||||
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
|
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
|
||||||
macData[20] = (byte) 0x80; // elements are terminated by 0x80 and zero-padded to the next block
|
macData[20] = -128; // 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
|
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
|
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
|
||||||
byte[] paddedData = Arrays.copyOf(data, length);
|
byte[] paddedData = Arrays.copyOf(data, length);
|
||||||
paddedData[data.length] = (byte) 0x80;
|
paddedData[data.length] = -128;
|
||||||
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
|
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
|
||||||
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
|
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
|
||||||
}
|
}
|
||||||
macData[35 + encryptedData.length] = (byte) 0x80;
|
macData[35 + encryptedData.length] = -128;
|
||||||
byte[] MAC = getMAC(macData, keyMAC);
|
byte[] MAC = getMAC(macData, keyMAC);
|
||||||
|
|
||||||
// construct the APDU using the encrypted data and the MAC
|
// construct the APDU using the encrypted data and the MAC
|
||||||
byte[] APDU = Arrays.copyOf(incomplete, incomplete.length + encryptedData.length + MAC.length + 3);
|
byte[] APDU = new byte[incomplete.length + encryptedData.length + MAC.length + 3];
|
||||||
|
System.arraycopy(incomplete, 0, APDU, 0, incomplete.length);
|
||||||
if (encryptedData.length > 0) {
|
if (encryptedData.length > 0) {
|
||||||
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
|
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
|
||||||
}
|
}
|
||||||
System.arraycopy(new byte[]{(byte) 0x8E, 0x08}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
|
System.arraycopy(new byte[]{-114, 8}, 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);
|
System.arraycopy(MAC, 0, APDU, incomplete.length + encryptedData.length + 2, MAC.length);
|
||||||
|
|
||||||
ssc++;
|
ssc++;
|
||||||
return APDU;
|
return APDU;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects a FILE by its identifier
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the contents of the personal data dedicated file
|
* Gets the contents of the personal data dedicated file
|
||||||
*
|
*
|
||||||
* @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16)
|
* @param FID the last bytes of file identifiers being requested
|
||||||
* @return array containing the corresponding data strings
|
* @return array containing the data strings
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public String[] readPersonalData(byte[] lastBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
public String[] readPersonalData(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||||
|
|
||||||
String[] personalData = new String[lastBytes.length];
|
String[] personalData = new String[FID.length];
|
||||||
int stringIndex = 0;
|
byte[] data;
|
||||||
|
byte[] APDU;
|
||||||
// select the master application
|
byte[] response;
|
||||||
selectFile(IASECCFID, "the master application");
|
|
||||||
|
|
||||||
// select the personal data dedicated file
|
// select the personal data dedicated file
|
||||||
selectFile(personalDF, "the personal data DF");
|
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));
|
||||||
|
|
||||||
byte[] FID = Arrays.copyOf(personalDF, personalDF.length);
|
// select and read the first 8 elementary files in the DF
|
||||||
// select and read the personal data elementary files
|
for (int i = 0; i < FID.length; i++) {
|
||||||
for (byte index : lastBytes) {
|
|
||||||
|
|
||||||
if (index > 15 || index < 1) throw new RuntimeException("Invalid personal data FID.");
|
byte index = FID[i];
|
||||||
FID[1] = index;
|
if (index > 15 || index < 1) throw new AuthAppException("Invalid personal data FID.", 500);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
// store the decrypted datum
|
// store the decrypted datum
|
||||||
byte[] response = readFile(FID, "a personal data EF");
|
byte[] raw = encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
|
||||||
int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2;
|
int indexOfTerminator = Hex.toHexString(raw).lastIndexOf("80") / 2;
|
||||||
personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator));
|
personalData[i] = new String(Arrays.copyOfRange(raw, 0, indexOfTerminator));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return personalData;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -19,23 +19,24 @@ class Pin2Fragment : Fragment() {
|
|||||||
|
|
||||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentPin2Binding? = null
|
private var _binding: FragmentPin2Binding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentPin2Binding.inflate(inflater, container, false)
|
_binding = FragmentPin2Binding.inflate(inflater, container, false)
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding!!.nextButton.setOnClickListener {
|
binding.nextButton.setOnClickListener {
|
||||||
checkPin2Length()
|
checkPin2Length()
|
||||||
}
|
}
|
||||||
binding!!.cancelButton.setOnClickListener {
|
binding.cancelButton.setOnClickListener {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ class Pin2Fragment : Fragment() {
|
|||||||
* then it is saved to the viewModel.
|
* then it is saved to the viewModel.
|
||||||
*/
|
*/
|
||||||
private fun checkPin2Length() {
|
private fun checkPin2Length() {
|
||||||
val enteredPin2 = binding!!.pin2EditText.editText?.text.toString()
|
val enteredPin2 = binding.pin2EditText.editText?.text.toString()
|
||||||
if (enteredPin2.length in 5..12) {
|
if (enteredPin2.length in 5..12) {
|
||||||
viewModel.setUserPin2(enteredPin2)
|
viewModel.setUserPin2(enteredPin2)
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +67,7 @@ class Pin2Fragment : Fragment() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -1,17 +1,22 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp
|
package com.tarkvaraprojekt.mobileauthapp
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
|
|
||||||
@@ -24,121 +29,129 @@ class PinFragment : Fragment() {
|
|||||||
|
|
||||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentPinBinding? = null
|
private var _binding: FragmentPinBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
// Navigation arguments:
|
// Navigation arguments:
|
||||||
// saving = true means that the user must be returned to the settings menu
|
// saving = true means that the user must be returned to the settings menu
|
||||||
// reading = true means that we are reading information from the ID card that does
|
|
||||||
// not require PIN 1 so it is not necessary to ask it.
|
|
||||||
private val args: PinFragmentArgs by navArgs()
|
private val args: PinFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
private var saveToggle = true
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentPinBinding.inflate(inflater, container, false)
|
_binding = FragmentPinBinding.inflate(inflater, container, false)
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
checkIfSkip()
|
checkIfSkip()
|
||||||
// If the user arrives from the settings menu then the button says
|
// Switch should be not visible when user is in savings mode
|
||||||
// save instead of continue.
|
|
||||||
if (args.saving) {
|
if (args.saving) {
|
||||||
binding!!.nextButton.text = getString(R.string.save_text)
|
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!!.nextButton.setOnClickListener { checkEnteredPin() }
|
binding.saveSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding!!.cancelButton.setOnClickListener { goToTheStart() }
|
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
|
||||||
/**
|
|
||||||
* 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 (args.reading) {
|
|
||||||
goToTheNextFragment()
|
|
||||||
} else if (viewModel.userPin.length in 4..12) {
|
|
||||||
goToTheNextFragment()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.buttonContinue.setOnClickListener { checkEnteredPin() }
|
||||||
|
binding.buttonCancel.setOnClickListener { goToTheStart() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes user to the next fragment, which is AuthFragment.
|
* Takes user to the next fragment, which is AuthFragment.
|
||||||
*/
|
*/
|
||||||
private fun goToTheNextFragment() {
|
private fun goToTheNextFragment() {
|
||||||
val action = PinFragmentDirections.actionPinFragmentToAuthFragment(reading = args.reading, auth = args.auth, mobile = args.mobile)
|
val action = PinFragmentDirections.actionPinFragmentToAuthFragment(auth = args.auth, mobile = args.mobile)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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!!.pinEditText.editText?.text.toString()
|
|
||||||
if (enteredPin.length in 4..12) {
|
|
||||||
viewModel.setUserPin(enteredPin)
|
|
||||||
if (args.saving) {
|
|
||||||
viewModel.storePin(requireContext())
|
|
||||||
goToTheStart()
|
|
||||||
} else {
|
|
||||||
val storePinQuestion = getDialog()
|
|
||||||
storePinQuestion?.show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.length_pin), Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a dialog that asks the user whether the entered PIN 1 should be saved
|
|
||||||
* on the device or not.
|
|
||||||
*/
|
|
||||||
private fun getDialog(): AlertDialog? {
|
|
||||||
return activity?.let { frag ->
|
|
||||||
val builder = AlertDialog.Builder(frag)
|
|
||||||
builder.apply {
|
|
||||||
// If response is positive save the PIN 1 on the device.
|
|
||||||
setPositiveButton(R.string.save_text) { _, _ ->
|
|
||||||
viewModel.storePin(
|
|
||||||
requireContext()
|
|
||||||
)
|
|
||||||
goToTheNextFragment()
|
|
||||||
}
|
|
||||||
setNegativeButton(R.string.deny_text) { _, _ ->
|
|
||||||
goToTheNextFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.setMessage(R.string.pin_save_request)
|
|
||||||
builder.setTitle(R.string.save_pin_title)
|
|
||||||
builder.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns user to the start. If the user arrived from the settings menu then the start is
|
* Returns user to the start. If the user arrived from the settings menu then the start is
|
||||||
* settings menu not the HomeFragment.
|
* settings menu not the HomeFragment.
|
||||||
*/
|
*/
|
||||||
private fun goToTheStart() {
|
private fun goToTheStart() {
|
||||||
if (args.saving) {
|
if (args.saving) {
|
||||||
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
|
findNavController().navigate(R.id.action_pinFragment_to_settingsFragment)
|
||||||
} else if (args.auth) {
|
} else if (args.auth || args.mobile) {
|
||||||
|
if (args.mobile) {
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
|
} else {
|
||||||
|
requireActivity().finishAndRemoveTask()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
findNavController().navigate(R.id.action_canFragment_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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -14,15 +14,6 @@ import com.google.gson.JsonObject
|
|||||||
import com.koushikdutta.ion.Ion
|
import com.koushikdutta.ion.Ion
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
|
||||||
import com.tarkvaraprojekt.mobileauthapp.network.BASE_URL
|
|
||||||
import com.tarkvaraprojekt.mobileauthapp.network.TokenApi
|
|
||||||
import com.tarkvaraprojekt.mobileauthapp.network.TokenApiService
|
|
||||||
import com.tarkvaraprojekt.mobileauthapp.network.TokenItem
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResultFragment is used to create a JWT and to send response to the website/application
|
* ResultFragment is used to create a JWT and to send response to the website/application
|
||||||
@@ -33,7 +24,8 @@ class ResultFragment : Fragment() {
|
|||||||
|
|
||||||
private val paramsModel: ParametersViewModel by activityViewModels()
|
private val paramsModel: ParametersViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentResultBinding? = null
|
private var _binding: FragmentResultBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val args: ResultFragmentArgs by navArgs()
|
private val args: ResultFragmentArgs by navArgs()
|
||||||
|
|
||||||
@@ -42,18 +34,25 @@ class ResultFragment : Fragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentResultBinding.inflate(inflater, container, false)
|
_binding = FragmentResultBinding.inflate(inflater, container, false)
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding!!.resultBackButton.setOnClickListener {
|
|
||||||
// if (args.mobile) {
|
|
||||||
// createResponse()
|
|
||||||
// }
|
|
||||||
postToken()
|
postToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only used when the MobileAuthApp was launched by an app. Not for website use.
|
||||||
|
*/
|
||||||
|
private fun createResponse(success: Boolean = true, result: String = "noResult", token: String = "noToken") {
|
||||||
|
val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
||||||
|
val resultIntent = Intent()
|
||||||
|
resultIntent.putExtra("result", result)
|
||||||
|
resultIntent.putExtra("token", token)
|
||||||
|
requireActivity().setResult(responseCode, resultIntent)
|
||||||
|
requireActivity().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,47 +63,31 @@ class ResultFragment : Fragment() {
|
|||||||
json.addProperty("token", paramsModel.token)
|
json.addProperty("token", paramsModel.token)
|
||||||
json.addProperty("challenge", paramsModel.challenge)
|
json.addProperty("challenge", paramsModel.challenge)
|
||||||
|
|
||||||
Ion.getDefault(activity).getConscryptMiddleware().enable(false)
|
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||||
|
|
||||||
Ion.with(activity)
|
Ion.with(activity)
|
||||||
.load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication")
|
.load(paramsModel.origin + paramsModel.authUrl)
|
||||||
.setJsonObjectBody(json)
|
.setJsonObjectBody(json)
|
||||||
.asJsonObject()
|
.asJsonObject()
|
||||||
.setCallback { e, result ->
|
.setCallback { e, result ->
|
||||||
// do stuff with the result or error
|
if (result == null) {
|
||||||
Log.i("Log thingy", result.toString())
|
if (args.mobile) {
|
||||||
|
createResponse(false)
|
||||||
|
} else {
|
||||||
|
requireActivity().finishAndRemoveTask()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (args.mobile) {
|
||||||
|
createResponse(true, result.toString(), paramsModel.token)
|
||||||
|
} else {
|
||||||
|
requireActivity().finishAndRemoveTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
// val response = TokenApi.retrofitService.postToken(jsonBody)
|
|
||||||
// Log.v("Response", response.message())
|
|
||||||
// if (response.isSuccessful) {
|
|
||||||
// //Success scenario here
|
|
||||||
// } else {
|
|
||||||
// //Failure scenario here
|
|
||||||
// if (args.mobile) {
|
|
||||||
// createResponse(false)
|
|
||||||
// } else {
|
|
||||||
// //Currently for some reason the activity is not killed entirely. Must be looked into further.
|
|
||||||
// requireActivity().finish()
|
|
||||||
// exitProcess(0)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Only used when the MobileAuthApp was launched by an app. Not for website use.
|
|
||||||
*/
|
|
||||||
private fun createResponse(success: Boolean = true) {
|
|
||||||
val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
|
||||||
val resultIntent = Intent()
|
|
||||||
requireActivity().setResult(responseCode, resultIntent)
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -20,33 +20,34 @@ class UserFragment : Fragment() {
|
|||||||
|
|
||||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentUserBinding? = null
|
private var _binding: FragmentUserBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentUserBinding.inflate(inflater, container, false)
|
_binding = FragmentUserBinding.inflate(inflater, container, false)
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
displayInformation()
|
displayInformation()
|
||||||
binding!!.clearButton.setOnClickListener { goToTheStart() }
|
binding.clearButton.setOnClickListener { goToTheStart() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns text values to the fields in order to display user information.
|
* Assigns text values to the fields in order to display user information.
|
||||||
*/
|
*/
|
||||||
private fun displayInformation() {
|
private fun displayInformation() {
|
||||||
binding!!.userName.text =
|
binding.userName.text =
|
||||||
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
|
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
|
||||||
binding!!.identificationNumber.text = viewModel.userIdentificationNumber
|
binding.identificationNumber.text = viewModel.userIdentificationNumber
|
||||||
binding!!.gender.text = viewModel.gender
|
binding.gender.text = viewModel.gender
|
||||||
binding!!.expiration.text = viewModel.expiration.replace(" ", "/")
|
binding.expiration.text = viewModel.expiration.replace(" ", "/")
|
||||||
binding!!.citizenship.text = viewModel.citizenship
|
binding.citizenship.text = viewModel.citizenship
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,6 +60,6 @@ class UserFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package com.tarkvaraprojekt.mobileauthapp.auth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A specialised RuntimeException class for exceptions related to the mobile authentication app.
|
||||||
|
* Possible error codes can be found at
|
||||||
|
* https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Error-codes
|
||||||
|
* @param message Error message
|
||||||
|
* @param code An error code defined in the project wiki
|
||||||
|
*/
|
||||||
|
open class AuthAppException(message: String, var code: Int) : RuntimeException(message)
|
@@ -36,7 +36,7 @@ class Authenticator(val comms : Comms) {
|
|||||||
// Get header and claims.
|
// Get header and claims.
|
||||||
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
|
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
|
||||||
val claims =
|
val claims =
|
||||||
"""{"iat":"$epoch","exp":"$exp","aud":"$originUrl","iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
"""{"iat":"$epoch","exp":"$exp","aud":["$originUrl"],"iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
||||||
|
|
||||||
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
|
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
|
||||||
claims.toByteArray(Charsets.UTF_8)
|
claims.toByteArray(Charsets.UTF_8)
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
package com.tarkvaraprojekt.mobileauthapp.auth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AuthAppException for when the user entered CAN does not match the one read from the ID-card
|
||||||
|
* @see AuthAppException
|
||||||
|
*/
|
||||||
|
class InvalidCANException : AuthAppException("Invalid CAN", 400)
|
@@ -1,13 +1,17 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp.menu
|
package com.tarkvaraprojekt.mobileauthapp.menu
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
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.R
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentSettingsBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentSettingsBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
@@ -21,7 +25,8 @@ class SettingsFragment : Fragment() {
|
|||||||
|
|
||||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||||
|
|
||||||
private var binding: FragmentSettingsBinding? = null
|
private var _binding: FragmentSettingsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private var showPin: Boolean = false
|
private var showPin: Boolean = false
|
||||||
|
|
||||||
@@ -30,8 +35,8 @@ class SettingsFragment : Fragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
||||||
return binding!!.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -39,10 +44,20 @@ class SettingsFragment : Fragment() {
|
|||||||
showCanField()
|
showCanField()
|
||||||
showPinField()
|
showPinField()
|
||||||
togglePinButton()
|
togglePinButton()
|
||||||
binding!!.canMenuAction.setOnClickListener { canAction() }
|
binding.canMenuAction.setOnClickListener { canAction() }
|
||||||
binding!!.pinMenuAction.setOnClickListener { pinAction() }
|
binding.pinMenuAction.setOnClickListener { pinAction() }
|
||||||
binding!!.pinMenuShow.setOnClickListener { togglePin() }
|
binding.pinMenuShow.setOnClickListener { togglePin() }
|
||||||
binding!!.returnButton.setOnClickListener { backToHome() }
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,11 +65,11 @@ class SettingsFragment : Fragment() {
|
|||||||
*/
|
*/
|
||||||
private fun showCanField() {
|
private fun showCanField() {
|
||||||
if (viewModel.userCan.length == 6) {
|
if (viewModel.userCan.length == 6) {
|
||||||
binding!!.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
|
binding.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
|
||||||
binding!!.canMenuAction.text = getString(R.string.can_delete)
|
binding.canMenuAction.text = getString(R.string.can_delete)
|
||||||
} else {
|
} else {
|
||||||
binding!!.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
|
binding.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
|
||||||
binding!!.canMenuAction.text = getString(R.string.can_add)
|
binding.canMenuAction.text = getString(R.string.add_can_text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +81,9 @@ class SettingsFragment : Fragment() {
|
|||||||
if (viewModel.userCan.length == 6) {
|
if (viewModel.userCan.length == 6) {
|
||||||
viewModel.deleteCan(requireContext())
|
viewModel.deleteCan(requireContext())
|
||||||
showCanField()
|
showCanField()
|
||||||
|
showSnackbar(getString(R.string.can_deleted))
|
||||||
} else {
|
} else {
|
||||||
|
(activity as MainActivity).inMenu = false
|
||||||
val action = SettingsFragmentDirections.actionSettingsFragmentToCanFragment(saving = true)
|
val action = SettingsFragmentDirections.actionSettingsFragmentToCanFragment(saving = true)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
@@ -79,16 +96,16 @@ class SettingsFragment : Fragment() {
|
|||||||
*/
|
*/
|
||||||
private fun showPinField() {
|
private fun showPinField() {
|
||||||
if (viewModel.userPin.length in 4..12) {
|
if (viewModel.userPin.length in 4..12) {
|
||||||
binding!!.pinMenuShow.visibility = Button.VISIBLE
|
binding.pinMenuShow.visibility = Button.VISIBLE
|
||||||
if (showPin)
|
if (showPin)
|
||||||
binding!!.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
|
binding.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
|
||||||
else
|
else
|
||||||
binding!!.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
|
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
|
||||||
binding!!.pinMenuAction.text = getString(R.string.pin1_delete)
|
binding.pinMenuAction.text = getString(R.string.pin1_delete)
|
||||||
} else {
|
} else {
|
||||||
binding!!.pinMenuShow.visibility = Button.GONE
|
binding.pinMenuShow.visibility = Button.GONE
|
||||||
binding!!.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
|
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
|
||||||
binding!!.pinMenuAction.text = getString(R.string.pin1_add)
|
binding.pinMenuAction.text = getString(R.string.pin1_add)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +117,9 @@ class SettingsFragment : Fragment() {
|
|||||||
if (viewModel.userPin.length in 4..12) {
|
if (viewModel.userPin.length in 4..12) {
|
||||||
viewModel.deletePin(requireContext())
|
viewModel.deletePin(requireContext())
|
||||||
showPinField()
|
showPinField()
|
||||||
|
showSnackbar(getString(R.string.pin_deleted))
|
||||||
} else {
|
} else {
|
||||||
|
(activity as MainActivity).inMenu = false
|
||||||
val action = SettingsFragmentDirections.actionSettingsFragmentToPinFragment(saving = true)
|
val action = SettingsFragmentDirections.actionSettingsFragmentToPinFragment(saving = true)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
@@ -120,9 +139,9 @@ class SettingsFragment : Fragment() {
|
|||||||
*/
|
*/
|
||||||
private fun togglePinButton() {
|
private fun togglePinButton() {
|
||||||
if (showPin) {
|
if (showPin) {
|
||||||
binding!!.pinMenuShow.text = getString(R.string.hide)
|
binding.pinMenuShow.text = getString(R.string.hide)
|
||||||
} else {
|
} else {
|
||||||
binding!!.pinMenuShow.text = getString(R.string.show)
|
binding.pinMenuShow.text = getString(R.string.show)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +149,13 @@ class SettingsFragment : Fragment() {
|
|||||||
* Navigates back to home fragment.
|
* Navigates back to home fragment.
|
||||||
*/
|
*/
|
||||||
private fun backToHome() {
|
private fun backToHome() {
|
||||||
|
(activity as MainActivity).inMenu = false
|
||||||
findNavController().navigate(R.id.action_settingsFragment_to_homeFragment)
|
findNavController().navigate(R.id.action_settingsFragment_to_homeFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -13,6 +13,9 @@ class ParametersViewModel: ViewModel() {
|
|||||||
private var _token: String = ""
|
private var _token: String = ""
|
||||||
val token get() = _token
|
val token get() = _token
|
||||||
|
|
||||||
|
private var _origin: String = ""
|
||||||
|
val origin get() = _origin
|
||||||
|
|
||||||
fun setChallenge(newChallenge: String) {
|
fun setChallenge(newChallenge: String) {
|
||||||
_challenge = newChallenge
|
_challenge = newChallenge
|
||||||
}
|
}
|
||||||
@@ -24,4 +27,8 @@ class ParametersViewModel: ViewModel() {
|
|||||||
fun setToken(newToken: String) {
|
fun setToken(newToken: String) {
|
||||||
_token = newToken
|
_token = newToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOrigin(newOrigin: String) {
|
||||||
|
_origin = newOrigin
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,34 +0,0 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp.network
|
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
|
||||||
import retrofit2.http.Body
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Headers
|
|
||||||
import retrofit2.http.POST
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for making HTTP requests
|
|
||||||
* Based on https://developer.android.com/courses/pathways/android-basics-kotlin-unit-4-pathway-2
|
|
||||||
*/
|
|
||||||
const val BASE_URL =
|
|
||||||
"https://6bb0-85-253-195-252.ngrok.io"
|
|
||||||
|
|
||||||
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
|
||||||
private val retrofit = Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi))
|
|
||||||
.baseUrl(BASE_URL).build()
|
|
||||||
|
|
||||||
interface TokenApiService {
|
|
||||||
@Headers("Content-Type: application/json")
|
|
||||||
@POST("/auth/authentication")
|
|
||||||
suspend fun postToken(@Body data: String): Response<TokenItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
object TokenApi {
|
|
||||||
val retrofitService : TokenApiService by lazy {
|
|
||||||
retrofit.create(TokenApiService::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp.network
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TokenItem for making POST request.
|
|
||||||
*/
|
|
||||||
data class TokenItem (
|
|
||||||
val token: String,
|
|
||||||
val challenge: String,
|
|
||||||
)
|
|
@@ -5,7 +5,7 @@
|
|||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#3DDC84"
|
android:fillColor="#001970"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
|
@@ -4,14 +4,14 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="@dimen/padding"
|
||||||
tools:context=".AuthFragment">
|
tools:context=".AuthFragment">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/card_view"
|
android:id="@+id/card_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="@dimen/margin"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
@@ -23,31 +23,32 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="20sp">
|
android:padding="@dimen/padding_small">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/auth_fragment_instruction"
|
android:id="@+id/auth_fragment_instruction"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="6dp"
|
android:layout_margin="@dimen/margin"
|
||||||
android:gravity="center"
|
android:gravity="left"
|
||||||
android:text="@string/auth_instruction_text"
|
android:text="@string/auth_instruction_text"
|
||||||
android:textSize="20sp" />
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/nfc_logo"
|
android:id="@+id/nfc_logo"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="@dimen/logo_big"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="@dimen/logo_big"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_margin="6dp"
|
android:layout_margin="@dimen/margin"
|
||||||
|
android:padding="@dimen/margin_huge"
|
||||||
android:src="@drawable/nfc_logo" />
|
android:src="@drawable/nfc_logo" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/time_counter"
|
android:id="@+id/time_counter"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="6dp"
|
android:layout_margin="@dimen/margin"
|
||||||
android:textSize="14sp"
|
android:textSize="@dimen/regular_text"
|
||||||
app:layout_constraintTop_toBottomOf="@id/auth_fragment_instruction"
|
app:layout_constraintTop_toBottomOf="@id/auth_fragment_instruction"
|
||||||
tools:text="@string/time_left" />
|
tools:text="@string/time_left" />
|
||||||
|
|
||||||
@@ -60,9 +61,9 @@
|
|||||||
android:id="@+id/next_button"
|
android:id="@+id/next_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="@dimen/margin_big"
|
||||||
android:text="@string/next_text"
|
android:text="@string/continue_button"
|
||||||
android:textSize="15sp"
|
android:textSize="@dimen/regular_text"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||||
@@ -71,10 +72,10 @@
|
|||||||
android:id="@+id/cancel_button"
|
android:id="@+id/cancel_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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:text="@string/cancel_text"
|
||||||
android:textSize="15sp"
|
android:textSize="@dimen/regular_text"
|
||||||
app:layout_constraintEnd_toStartOf="@id/next_button"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||||
|
|
||||||
|
@@ -4,82 +4,57 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="@dimen/padding"
|
||||||
tools:context=".CanFragment">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<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="20sp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/enter_can"
|
android:id="@+id/title_text"
|
||||||
|
android:text="@string/can_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="6dp"
|
android:textSize="@dimen/headline_text"
|
||||||
android:text="@string/enter_can"
|
android:layout_margin="@dimen/margin_big"
|
||||||
android:textSize="20sp" />
|
android:fontFamily="sans-serif"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/can_edit_text"
|
android:id="@+id/canTextField"
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="6dp"
|
android:layout_margin="@dimen/margin_big"
|
||||||
android:hint="@string/text_can"
|
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:counterEnabled="true"
|
||||||
app:counterMaxLength="6"
|
app:counterMaxLength="6"
|
||||||
app:endIconMode="password_toggle"
|
app:counterTextAppearance="@style/helper"
|
||||||
app:errorEnabled="true"
|
app:counterOverflowTextAppearance="@style/helper"
|
||||||
app:helperText="@string/example_can"
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
app:helperTextEnabled="true"
|
|
||||||
app:startIconDrawable="@drawable/can_logo">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="numberPassword"
|
android:textSize="@dimen/regular_text"
|
||||||
android:textSize="14sp" />
|
android:fontFamily="sans-serif"
|
||||||
|
android:inputType="number"
|
||||||
|
android:singleLine="true"
|
||||||
|
/>
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/next_button"
|
android:id="@+id/button_cancel"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:text="@string/next_text"
|
|
||||||
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:text="@string/cancel_text"
|
||||||
android:textSize="15sp"
|
android:textSize="@dimen/regular_text"
|
||||||
app:layout_constraintEnd_toStartOf="@id/next_button"
|
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
app:layout_constraintTop_toBottomOf="@id/canTextField" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -4,7 +4,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="@dimen/padding"
|
||||||
tools:context=".HomeFragment">
|
tools:context=".HomeFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
android:id="@+id/can_status"
|
android:id="@+id/can_status"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="12dp"
|
android:layout_margin="@dimen/margin_big"
|
||||||
app:strokeWidth="1dp"
|
app:strokeWidth="1dp"
|
||||||
app:strokeColor="@color/stroke_color"
|
app:strokeColor="@color/stroke_color"
|
||||||
app:cardElevation="0dp">
|
app:cardElevation="0dp">
|
||||||
@@ -32,14 +32,14 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/can_status_logo"
|
android:id="@+id/can_status_logo"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="@dimen/margin"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/can_status_text"
|
android:id="@+id/can_status_text"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:padding="12dp"
|
android:padding="@dimen/margin"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
android:id="@+id/pin_status"
|
android:id="@+id/pin_status"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="12dp"
|
android:layout_margin="@dimen/margin_big"
|
||||||
app:strokeWidth="1dp"
|
app:strokeWidth="1dp"
|
||||||
app:strokeColor="@color/stroke_color"
|
app:strokeColor="@color/stroke_color"
|
||||||
app:cardElevation="0dp">
|
app:cardElevation="0dp">
|
||||||
@@ -63,14 +63,14 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pin_status_logo"
|
android:id="@+id/pin_status_logo"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="@dimen/margin"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pin_status_text"
|
android:id="@+id/pin_status_text"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:padding="12dp"
|
android:padding="@dimen/margin"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
@@ -80,15 +80,49 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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>
|
||||||
|
|
||||||
<Button
|
<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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/begin_text"
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginStart="@dimen/margin_huge"
|
||||||
android:textSize="15sp"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toBottomOf="@id/saved_states"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -4,81 +4,111 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="@dimen/padding"
|
||||||
tools:context=".PinFragment">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<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
|
<TextView
|
||||||
android:id="@+id/pin_fragment_text"
|
android:id="@+id/title_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="6dp"
|
android:layout_margin="@dimen/margin_big"
|
||||||
android:text="@string/pin_fragment" />
|
android:fontFamily="sans-serif"
|
||||||
|
android:text="@string/pin_view"
|
||||||
|
android:textSize="@dimen/headline_text"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/pin_edit_text"
|
android:id="@+id/pinTextField"
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="6dp"
|
android:layout_margin="@dimen/margin_big"
|
||||||
android:hint="@string/enter_pin"
|
android:hint="@string/hint_pin"
|
||||||
app:counterEnabled="true"
|
app:counterEnabled="true"
|
||||||
app:counterMaxLength="12"
|
app:counterMaxLength="12"
|
||||||
|
app:counterOverflowTextAppearance="@style/helper"
|
||||||
|
app:counterTextAppearance="@style/helper"
|
||||||
app:endIconMode="password_toggle"
|
app:endIconMode="password_toggle"
|
||||||
app:errorEnabled="true"
|
app:helperText="@string/pin_helper_text"
|
||||||
app:helperText="@string/example_pin"
|
|
||||||
app:helperTextEnabled="true"
|
app:helperTextEnabled="true"
|
||||||
app:startIconDrawable="@drawable/can_logo">
|
app:helperTextTextAppearance="@style/helper"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title_text">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif"
|
||||||
android:inputType="numberPassword"
|
android:inputType="numberPassword"
|
||||||
android:textSize="14sp" />
|
android:singleLine="true"
|
||||||
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/save_pin_question"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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/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>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/next_button"
|
android:id="@+id/button_continue"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_margin="@dimen/margin_big"
|
||||||
android:text="@string/next_text"
|
android:fontFamily="sans-serif"
|
||||||
android:textSize="15sp"
|
android:text="@string/continue_button"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:textSize="@dimen/regular_text"
|
||||||
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -64,7 +64,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:text="@string/next_text"
|
android:text="@string/continue_button"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
||||||
|
@@ -4,14 +4,14 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="@dimen/padding"
|
||||||
tools:context=".ResultFragment">
|
tools:context=".ResultFragment">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/can_status"
|
android:id="@+id/can_status"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="12dp"
|
android:layout_margin="@dimen/margin"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@@ -27,33 +27,23 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/result_text"
|
android:id="@+id/result_text"
|
||||||
android:text="@string/result_text"
|
android:text="@string/result_text"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:padding="12dp"
|
android:padding="@dimen/padding_small"
|
||||||
android:layout_marginVertical="6dp"
|
android:layout_marginVertical="@dimen/margin_small"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/result_info_text"
|
android:id="@+id/result_info_text"
|
||||||
android:text="@string/result_info"
|
android:text="@string/result_info"
|
||||||
android:padding="12dp"
|
android:padding="@dimen/padding_small"
|
||||||
android:textSize="16sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:layout_marginVertical="6dp"
|
android:layout_marginVertical="@dimen/margin_small"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/result_back_button"
|
|
||||||
android:text="@string/return_text"
|
|
||||||
android:layout_marginHorizontal="12dp"
|
|
||||||
android:layout_marginVertical="6dp"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -4,7 +4,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_margin="12dp"
|
android:layout_margin="@dimen/padding"
|
||||||
tools:context=".menu.SettingsFragment">
|
tools:context=".menu.SettingsFragment">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
@@ -22,44 +22,46 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="12dp">
|
android:padding="@dimen/padding_small">
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/can_saved"
|
android:id="@+id/can_saved"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:padding="12dp"
|
android:padding="@dimen/margin_small"
|
||||||
android:text="@string/saved_can" />
|
android:text="@string/saved_can" />
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/can_menu_action"
|
android:id="@+id/can_menu_action"
|
||||||
android:layout_margin="12dp"
|
android:layout_margin="@dimen/margin_small"
|
||||||
android:textSize="15sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pin_saved"
|
android:id="@+id/pin_saved"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:padding="12dp"
|
android:padding="@dimen/margin_small"
|
||||||
android:text="@string/saved_pin"/>
|
android:text="@string/saved_pin"/>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="vertical">
|
||||||
<Button
|
|
||||||
android:id="@+id/pin_menu_action"
|
|
||||||
android:layout_margin="12dp"
|
|
||||||
android:textSize="15sp"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/pin_menu_show"
|
android:id="@+id/pin_menu_show"
|
||||||
android:layout_margin="12dp"
|
android:layout_marginHorizontal="@dimen/margin"
|
||||||
android:textSize="15sp"
|
android:layout_marginVertical="@dimen/margin_small"
|
||||||
|
android:textSize="@dimen/regular_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"/>
|
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>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -69,8 +71,9 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/return_text"
|
android:text="@string/return_text"
|
||||||
android:layout_margin="24dp"
|
android:layout_marginVertical="@dimen/margin"
|
||||||
android:textSize="15sp"
|
android:layout_marginStart="@dimen/padding"
|
||||||
|
android:textSize="@dimen/regular_text"
|
||||||
app:layout_constraintTop_toBottomOf="@id/settings_card"
|
app:layout_constraintTop_toBottomOf="@id/settings_card"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
@@ -1,17 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="24dp"
|
android:padding="@dimen/padding"
|
||||||
tools:context=".UserFragment">
|
tools:context=".UserFragment">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/card_view"
|
android:id="@+id/card_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="@dimen/margin"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
@@ -23,86 +26,86 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="20sp">
|
android:padding="@dimen/padding_tiny">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/user_name_label"
|
android:id="@+id/user_name_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/user_name_label"
|
android:text="@string/user_name_label"
|
||||||
android:textSize="14sp" />
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/user_name"
|
android:id="@+id/user_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
android:text="@string/user_name"
|
android:text="@string/user_name"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/identification_number_label"
|
android:id="@+id/identification_number_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="@dimen/margin_big"
|
||||||
android:text="@string/identification_number_label"
|
android:text="@string/identification_number_label"
|
||||||
android:textSize="14sp" />
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/identification_number"
|
android:id="@+id/identification_number"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/gender_label"
|
android:id="@+id/gender_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="@dimen/margin_big"
|
||||||
android:text="@string/gender_label"
|
android:text="@string/gender_label"
|
||||||
android:textSize="14sp" />
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/gender"
|
android:id="@+id/gender"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/expiration_label"
|
android:id="@+id/expiration_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="@dimen/margin_big"
|
||||||
android:text="@string/expiration_label"
|
android:text="@string/expiration_label"
|
||||||
android:textSize="14sp" />
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/expiration"
|
android:id="@+id/expiration"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/citizenship_label"
|
android:id="@+id/citizenship_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="@dimen/margin_big"
|
||||||
android:text="@string/citizenship_label"
|
android:text="@string/citizenship_label"
|
||||||
android:textSize="14sp" />
|
android:textSize="@dimen/regular_text" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/citizenship"
|
android:id="@+id/citizenship"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
android:textSize="20sp"
|
android:textSize="@dimen/regular_text"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -113,11 +116,12 @@
|
|||||||
android:id="@+id/clear_button"
|
android:id="@+id/clear_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="@dimen/margin_big"
|
||||||
android:text="@string/clear_button"
|
android:text="@string/return_text"
|
||||||
android:textSize="18sp"
|
android:textSize="@dimen/regular_text"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_check_logo" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_check_logo" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@@ -18,6 +18,9 @@
|
|||||||
android:id="@+id/action_homeFragment_to_canFragment"
|
android:id="@+id/action_homeFragment_to_canFragment"
|
||||||
app:destination="@id/canFragment"
|
app:destination="@id/canFragment"
|
||||||
app:popUpTo="@id/homeFragment" />
|
app:popUpTo="@id/homeFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_homeFragment_to_userFragment"
|
||||||
|
app:destination="@id/userFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/pinFragment"
|
android:id="@+id/pinFragment"
|
||||||
@@ -42,10 +45,6 @@
|
|||||||
android:id="@+id/action_pinFragment_to_authFragment"
|
android:id="@+id/action_pinFragment_to_authFragment"
|
||||||
app:destination="@id/authFragment"
|
app:destination="@id/authFragment"
|
||||||
app:popUpTo="@id/homeFragment" />
|
app:popUpTo="@id/homeFragment" />
|
||||||
<argument
|
|
||||||
android:name="reading"
|
|
||||||
app:argType="boolean"
|
|
||||||
android:defaultValue="false" />
|
|
||||||
<argument
|
<argument
|
||||||
android:name="auth"
|
android:name="auth"
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
@@ -78,10 +77,6 @@
|
|||||||
android:id="@+id/action_canFragment_to_pinFragment"
|
android:id="@+id/action_canFragment_to_pinFragment"
|
||||||
app:destination="@id/pinFragment"
|
app:destination="@id/pinFragment"
|
||||||
app:popUpTo="@id/homeFragment" />
|
app:popUpTo="@id/homeFragment" />
|
||||||
<argument
|
|
||||||
android:name="reading"
|
|
||||||
app:argType="boolean"
|
|
||||||
android:defaultValue="false" />
|
|
||||||
<argument
|
<argument
|
||||||
android:name="auth"
|
android:name="auth"
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
@@ -90,6 +85,10 @@
|
|||||||
android:name="mobile"
|
android:name="mobile"
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
|
<argument
|
||||||
|
android:name="fromhome"
|
||||||
|
app:argType="boolean"
|
||||||
|
android:defaultValue="false" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/authFragment"
|
android:id="@+id/authFragment"
|
||||||
@@ -113,10 +112,6 @@
|
|||||||
android:name="auth"
|
android:name="auth"
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
<argument
|
|
||||||
android:name="reading"
|
|
||||||
app:argType="boolean"
|
|
||||||
android:defaultValue="false" />
|
|
||||||
<argument
|
<argument
|
||||||
android:name="mobile"
|
android:name="mobile"
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
|
@@ -44,7 +44,9 @@
|
|||||||
<string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
|
<string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
|
||||||
<string name="time_left">Time left %d sek</string>
|
<string name="time_left">Time left %d sek</string>
|
||||||
<string name="no_time">No time left</string>
|
<string name="no_time">No time left</string>
|
||||||
<string name="no_success">Wrong CAN</string>
|
<string name="err_unknown">Unknown error</string>
|
||||||
|
<string name="invalid_can">Wrong CAN</string>
|
||||||
|
<string name="tag_lost">Connection between device and ID-card lost</string>
|
||||||
|
|
||||||
<!-- string resources for UserFragment layout -->
|
<!-- string resources for UserFragment layout -->
|
||||||
<string name="user_name_label">NAME</string>
|
<string name="user_name_label">NAME</string>
|
||||||
@@ -56,8 +58,8 @@
|
|||||||
<string name="clear_button">FORGET</string>
|
<string name="clear_button">FORGET</string>
|
||||||
|
|
||||||
<!-- string resources for ResultFragment layout-->
|
<!-- string resources for ResultFragment layout-->
|
||||||
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
|
<string name="result_text">Controlling the created token</string>
|
||||||
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
|
<string name="result_info">Wait for the app to close</string>
|
||||||
|
|
||||||
<!-- menu -->
|
<!-- menu -->
|
||||||
<string name="menu_settings_title">Settings</string>
|
<string name="menu_settings_title">Settings</string>
|
||||||
@@ -75,4 +77,7 @@
|
|||||||
<string name="hidden_pin">****</string>
|
<string name="hidden_pin">****</string>
|
||||||
<string name="unavailable">Settings currently unavailabe</string>
|
<string name="unavailable">Settings currently unavailabe</string>
|
||||||
<string name="can_save_request">CAN is currently not saved. Do you wish to save the CAN? Saved CAN will be entered automatically in the future. Saved CAN can be changed and deleted in the settings menu.</string>
|
<string name="can_save_request">CAN is currently not saved. Do you wish to save the CAN? Saved CAN will be entered automatically in the future. Saved CAN can be changed and deleted in the settings menu.</string>
|
||||||
|
<string name="err_reading_card">Failed to read data from the ID-card</string>
|
||||||
|
<string name="err_internal">Internal error</string>
|
||||||
|
<string name="err_bad_data">Read bad data from the ID-card, try using the card again</string>
|
||||||
</resources>
|
</resources>
|
@@ -43,7 +43,9 @@
|
|||||||
<string name="auth_instruction_text">ID kaardiga ühenduse loomiseks pane kaart vastu telefoni</string>
|
<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="time_left">Aega on jäänud %d sek</string>
|
||||||
<string name="no_time">Aeg on otsas</string>
|
<string name="no_time">Aeg on otsas</string>
|
||||||
<string name="no_success">Vale CAN</string>
|
<string name="err_unknown">Tundmatu viga</string>
|
||||||
|
<string name="invalid_can">Vale CAN</string>
|
||||||
|
<string name="tag_lost">Ühendus seadme ja kaardi vahel katkes</string>
|
||||||
|
|
||||||
<!-- string resources for UserFragment layout -->
|
<!-- string resources for UserFragment layout -->
|
||||||
<string name="user_name_label">NIMI</string>
|
<string name="user_name_label">NIMI</string>
|
||||||
@@ -55,8 +57,8 @@
|
|||||||
<string name="gender_label">SUGU</string>
|
<string name="gender_label">SUGU</string>
|
||||||
|
|
||||||
<!-- string resources for ResultFragment layout-->
|
<!-- string resources for ResultFragment layout-->
|
||||||
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
|
<string name="result_text">Tulemust kontrollitakse</string>
|
||||||
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
|
<string name="result_info">Rakendus sulgeb ennast ise</string>
|
||||||
|
|
||||||
<!-- menu -->
|
<!-- menu -->
|
||||||
<string name="menu_settings_title">Seaded</string>
|
<string name="menu_settings_title">Seaded</string>
|
||||||
@@ -73,4 +75,7 @@
|
|||||||
<string name="hide">PEIDA</string>
|
<string name="hide">PEIDA</string>
|
||||||
<string name="hidden_pin">****</string>
|
<string name="hidden_pin">****</string>
|
||||||
<string name="unavailable">Seaded pole hetkel saadaval</string>
|
<string name="unavailable">Seaded pole hetkel saadaval</string>
|
||||||
|
<string name="err_reading_card">Ei saanud ID-kaardilt andmeid lugeda</string>
|
||||||
|
<string name="err_internal">Rakendusesisene viga</string>
|
||||||
|
<string name="err_bad_data">ID-kaardilt loeti vigased andmed, proovi uuesti kaarti kasutada</string>
|
||||||
</resources>
|
</resources>
|
@@ -8,9 +8,9 @@
|
|||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
|
||||||
<color name="blue_200">#90caf9</color>
|
<color name="blue_200">#d1d9ff</color>
|
||||||
<color name="blue_500">#2196f3</color>
|
<color name="blue_500">#002984</color>
|
||||||
<color name="blue_700">#1976d2</color>
|
<color name="blue_700">#001970</color>
|
||||||
<color name="orange_200">#ffcc80</color>
|
<color name="orange_200">#ffab91</color>
|
||||||
<color name="orange_700">#f57c00</color>
|
<color name="orange_700">#f57c00</color>
|
||||||
</resources>
|
</resources>
|
15
MobileAuthApp/app/src/main/res/values/dimens.xml
Normal file
15
MobileAuthApp/app/src/main/res/values/dimens.xml
Normal 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>
|
@@ -43,7 +43,9 @@
|
|||||||
<string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
|
<string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
|
||||||
<string name="time_left">Time left %d sek</string>
|
<string name="time_left">Time left %d sek</string>
|
||||||
<string name="no_time">No time left</string>
|
<string name="no_time">No time left</string>
|
||||||
<string name="no_success">Wrong CAN</string>
|
<string name="err_unknown">Unknown error</string>
|
||||||
|
<string name="invalid_can">Wrong CAN</string>
|
||||||
|
<string name="tag_lost">Connection between device and ID-card lost</string>
|
||||||
|
|
||||||
<!-- string resources for UserFragment layout -->
|
<!-- string resources for UserFragment layout -->
|
||||||
<string name="user_name_label">NAME</string>
|
<string name="user_name_label">NAME</string>
|
||||||
@@ -55,8 +57,8 @@
|
|||||||
<string name="clear_button">FORGET</string>
|
<string name="clear_button">FORGET</string>
|
||||||
|
|
||||||
<!-- string resources for ResultFragment layout-->
|
<!-- string resources for ResultFragment layout-->
|
||||||
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
|
<string name="result_text">Controlling the created token</string>
|
||||||
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
|
<string name="result_info">Wait for the app to close</string>
|
||||||
|
|
||||||
<!-- menu -->
|
<!-- menu -->
|
||||||
<string name="menu_settings_title">Settings</string>
|
<string name="menu_settings_title">Settings</string>
|
||||||
@@ -73,4 +75,7 @@
|
|||||||
<string name="hide">HIDE</string>
|
<string name="hide">HIDE</string>
|
||||||
<string name="hidden_pin">****</string>
|
<string name="hidden_pin">****</string>
|
||||||
<string name="unavailable">Settings currently unavailable</string>
|
<string name="unavailable">Settings currently unavailable</string>
|
||||||
|
<string name="err_reading_card">Failed to read data from the ID-card</string>
|
||||||
|
<string name="err_internal">Internal error</string>
|
||||||
|
<string name="err_bad_data">Read bad data from the ID-card, try using the card again</string>
|
||||||
</resources>
|
</resources>
|
7
MobileAuthApp/app/src/main/res/values/styles.xml
Normal file
7
MobileAuthApp/app/src/main/res/values/styles.xml
Normal 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>
|
18
README.md
18
README.md
@@ -7,8 +7,6 @@ This is a proof-of-concept project for creating an Android app for authenticatin
|
|||||||
* The smartphone must support NFC technology and it must be enabled
|
* 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)
|
* The user must have an Estonian ID card with NFC capability (issued since 2018)
|
||||||
|
|
||||||
_Knowledge of Estonian might be useful as the application is in Estonian language at the moment. The English language support will be added during the iteration 3. If one does not understand Estonian then the guide at the bottom of the release notes might be useful._
|
|
||||||
|
|
||||||
### Installing the application on the phone
|
### 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.
|
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.
|
||||||
|
|
||||||
@@ -20,11 +18,15 @@ More info about installing third party applications on the Android phones: https
|
|||||||
|
|
||||||
**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.**
|
**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.**
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
### Wiki pages relevant for the "Software project" subject
|
### Wiki pages relevant for the "Software project" subject
|
||||||
* [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision) *last updated on 10.10*
|
* [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision)
|
||||||
* [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes) *last updated for iteration3 on 08.11*
|
* [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes)
|
||||||
* [Project tasks](https://tvp-mobile-authentication.atlassian.net/jira/software/projects/MOB/boards/1/backlog) (Ask Tanel for JIRA permissions if needed).
|
* [Project tasks](https://tvp-mobile-authentication.atlassian.net/jira/software/projects/MOB/boards/1/backlog) (Ask Tanel for JIRA permissions if needed).
|
||||||
* [Project plan](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-plan) *last updated on 10.10*
|
* [Project plan](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-plan)
|
||||||
* [Use Cases](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Cases) *last updated on 10.10*
|
* [Use Cases](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Cases)
|
||||||
* [User stories](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/User-stories) *last updated on 10.10*
|
* [User stories](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/User-stories)
|
||||||
* [Use Case Tests](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Case-Tests) *WIP*
|
* [Use Case Tests](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Case-Tests)
|
||||||
|
10
TestMobileApp/README.md
Normal file
10
TestMobileApp/README.md
Normal 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.
|
@@ -5,10 +5,17 @@ import android.content.Intent
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.example.testmobileapp.databinding.ActivityMainBinding
|
import com.example.testmobileapp.databinding.ActivityMainBinding
|
||||||
import com.koushikdutta.ion.Ion
|
import com.koushikdutta.ion.Ion
|
||||||
|
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-base-url-here"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test mobile app to demonstrate how other applications can use MobileAuthApp.
|
* Test mobile app to demonstrate how other applications can use MobileAuthApp.
|
||||||
@@ -18,35 +25,48 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var authLauncher: ActivityResultLauncher<Intent>
|
private lateinit var authLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
|
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
|
||||||
if (response.resultCode == Activity.RESULT_OK) {
|
if (response.resultCode == Activity.RESULT_OK) {
|
||||||
// Currently we are not actually checking whether we get a valid token.
|
|
||||||
// For testing purposes only, to make sure that we are able to get a response at all.
|
|
||||||
binding.loginTextView.text = getString(R.string.auth_success)
|
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("token").toString())
|
||||||
|
Log.i("getResult", response.data?.getStringExtra("result").toString())
|
||||||
|
var user = ""
|
||||||
|
try {
|
||||||
|
val resultObject = JSONObject(response.data?.getStringExtra("result").toString())
|
||||||
|
user = resultObject.getString("principal")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i("getResult", "unable to retrieve name from principal")
|
||||||
|
}
|
||||||
|
showResult(user)
|
||||||
}
|
}
|
||||||
if (response.resultCode == Activity.RESULT_CANCELED) {
|
if (response.resultCode == Activity.RESULT_CANCELED) {
|
||||||
binding.loginTextView.text = getString(R.string.auth_failure)
|
binding.loginTextView.text = getString(R.string.auth_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.loginOptionNfcButton.setOnClickListener { launchAuth() }
|
showLogin()
|
||||||
//binding.loginOptionNfcButton.setOnClickListener { getData() }
|
|
||||||
|
binding.loginOptionNfcButton.setOnClickListener { getData() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that creates an intent to launch the MobileAuthApp
|
* Method that creates an intent to launch the MobileAuthApp
|
||||||
*/
|
*/
|
||||||
private fun launchAuth(challenge: String = "challenge", authUrl: String = "authUrl") {
|
private fun launchAuth(challenge: String = "challenge", originUrl: String = "baseUrl", authUrl: String = "authUrl") {
|
||||||
val launchIntent = Intent()
|
val launchIntent = Intent()
|
||||||
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
|
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
|
||||||
launchIntent.putExtra("action", "auth")
|
launchIntent.putExtra("action", "auth")
|
||||||
launchIntent.putExtra("challenge", challenge)
|
launchIntent.putExtra("challenge", challenge)
|
||||||
|
launchIntent.putExtra("originUrl", originUrl)
|
||||||
launchIntent.putExtra("authUrl", authUrl)
|
launchIntent.putExtra("authUrl", authUrl)
|
||||||
launchIntent.putExtra("mobile", true)
|
launchIntent.putExtra("mobile", true)
|
||||||
authLauncher.launch(launchIntent)
|
authLauncher.launch(launchIntent)
|
||||||
@@ -58,19 +78,36 @@ class MainActivity : AppCompatActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun getData() {
|
private fun getData() {
|
||||||
// Enter the server endpoint address to here
|
// Enter the server endpoint address to here
|
||||||
val baseUrl = "enter-base-url-here"
|
val url = "$BASE_URL/auth/challenge"
|
||||||
val url = "$baseUrl/auth/challenge"
|
Ion.getDefault(this).conscryptMiddleware.enable(false)
|
||||||
Ion.with(applicationContext)
|
Ion.with(applicationContext)
|
||||||
.load(url)
|
.load(url)
|
||||||
.asJsonObject()
|
.asJsonObject()
|
||||||
.setCallback { _, result ->
|
.setCallback { _, result ->
|
||||||
try {
|
try {
|
||||||
// Get data from the result and call launchAuth method
|
// Get data from the result and call launchAuth method
|
||||||
val challenge = result.asJsonObject["nonce"].toString()
|
val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
|
||||||
launchAuth(challenge, baseUrl)
|
Log.v("Challenge", challenge)
|
||||||
|
launchAuth(challenge, BASE_URL, "/auth/authentication")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.i("GETrequest", "was unsuccessful")
|
Log.i("GETrequest", "was unsuccessful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -26,7 +26,8 @@
|
|||||||
android:layout_margin="12dp"
|
android:layout_margin="12dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/login_text_view"
|
app:layout_constraintTop_toBottomOf="@id/login_text_view"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/choose_method_text_view"
|
android:id="@+id/choose_method_text_view"
|
||||||
@@ -46,4 +47,30 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -4,6 +4,8 @@
|
|||||||
<string name="login_text">Login</string>
|
<string name="login_text">Login</string>
|
||||||
<string name="choose_login_method">Choose login method</string>
|
<string name="choose_login_method">Choose login method</string>
|
||||||
<string name="method_nfc">NFC auth</string>
|
<string name="method_nfc">NFC auth</string>
|
||||||
<string name="auth_success">Successful response</string>
|
<string name="auth_success">Logged in</string>
|
||||||
<string name="auth_failure">Response failed</string>
|
<string name="auth_failure">Response failed</string>
|
||||||
|
<string name="forget_button">Forget</string>
|
||||||
|
<string name="hello">Hello, %s!</string>
|
||||||
</resources>
|
</resources>
|
@@ -4,6 +4,8 @@
|
|||||||
<string name="login_text">Logi sisse</string>
|
<string name="login_text">Logi sisse</string>
|
||||||
<string name="choose_login_method">Vali sobiv meetod</string>
|
<string name="choose_login_method">Vali sobiv meetod</string>
|
||||||
<string name="method_nfc">NFC auth</string>
|
<string name="method_nfc">NFC auth</string>
|
||||||
<string name="auth_success">Vastus kätte saadud</string>
|
<string name="auth_success">Sisse logimine õnnestus</string>
|
||||||
<string name="auth_failure">Vastust ei õnnestunud kätte saada</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>
|
</resources>
|
@@ -3,6 +3,8 @@
|
|||||||
<string name="login_text">Login</string>
|
<string name="login_text">Login</string>
|
||||||
<string name="choose_login_method">Choose login method</string>
|
<string name="choose_login_method">Choose login method</string>
|
||||||
<string name="method_nfc">NFC auth</string>
|
<string name="method_nfc">NFC auth</string>
|
||||||
<string name="auth_success">Successful response</string>
|
<string name="auth_success">Logged in</string>
|
||||||
<string name="auth_failure">Response failed</string>
|
<string name="auth_failure">Response failed</string>
|
||||||
|
<string name="forget_button">Forget</string>
|
||||||
|
<string name="hello">Hello, %s!</string>
|
||||||
</resources>
|
</resources>
|
@@ -42,6 +42,11 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.9.0</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.webeid.security</groupId>
|
<groupId>org.webeid.security</groupId>
|
||||||
<artifactId>authtoken-validation</artifactId>
|
<artifactId>authtoken-validation</artifactId>
|
||||||
|
@@ -10,7 +10,7 @@ class ApplicationConfiguration {
|
|||||||
// Endpoint for authentication
|
// Endpoint for authentication
|
||||||
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
|
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
|
||||||
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
|
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
|
||||||
val WEBSITE_ORIGIN_URL = "https://6bb0-85-253-195-252.ngrok.io"
|
val WEBSITE_ORIGIN_URL = "https://5d0c-85-253-195-195.ngrok.io"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
package com.tarkvaratehnika.demobackend.config
|
package com.tarkvaratehnika.demobackend.config
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||||
@@ -28,14 +30,25 @@ import javax.cache.configuration.MutableConfiguration
|
|||||||
import javax.cache.expiry.CreatedExpiryPolicy
|
import javax.cache.expiry.CreatedExpiryPolicy
|
||||||
import javax.cache.expiry.Duration
|
import javax.cache.expiry.Duration
|
||||||
|
|
||||||
|
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class ValidationConfiguration {
|
class ValidationConfiguration {
|
||||||
|
|
||||||
|
private val LOG: Logger = LoggerFactory.getLogger(ValidationConfiguration::class.java)
|
||||||
|
|
||||||
private val NONCE_TTL_MINUTES: Long = 5
|
private val NONCE_TTL_MINUTES: Long = 5
|
||||||
private val CACHE_NAME = "nonceCache"
|
private val CACHE_NAME = "nonceCache"
|
||||||
private val CERTS_RESOURCE_PATH = "/certs/"
|
private val CERTS_RESOURCE_PATH = "/certs/"
|
||||||
private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
|
private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
|
||||||
private val TRUSTSTORE_PASSWORD = "changeit"
|
private val TRUSTSTORE_PASSWORD = "changeit"
|
||||||
|
companion object {
|
||||||
|
const val ROLE_USER : String = "ROLE_USER"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
LOG.warn("Creating new ValidationConfiguration.")
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun cacheManager(): CacheManager {
|
fun cacheManager(): CacheManager {
|
||||||
@@ -47,7 +60,9 @@ class ValidationConfiguration {
|
|||||||
val cacheManager: CacheManager = cacheManager()
|
val cacheManager: CacheManager = cacheManager()
|
||||||
var cache =
|
var cache =
|
||||||
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
|
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
|
||||||
|
|
||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
|
LOG.warn("Creating new cache.")
|
||||||
cache = createNonceCache(cacheManager)
|
cache = createNonceCache(cacheManager)
|
||||||
}
|
}
|
||||||
return cache
|
return cache
|
||||||
@@ -65,7 +80,7 @@ class ValidationConfiguration {
|
|||||||
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
|
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
|
||||||
.setTypes(String::class.java, ZonedDateTime::class.java)
|
.setTypes(String::class.java, ZonedDateTime::class.java)
|
||||||
.setExpiryPolicyFactory(
|
.setExpiryPolicyFactory(
|
||||||
FactoryBuilder.factoryOf(
|
factoryOf(
|
||||||
CreatedExpiryPolicy(
|
CreatedExpiryPolicy(
|
||||||
Duration(
|
Duration(
|
||||||
TimeUnit.MINUTES,
|
TimeUnit.MINUTES,
|
||||||
|
@@ -23,6 +23,9 @@
|
|||||||
package com.tarkvaratehnika.demobackend.security
|
package com.tarkvaratehnika.demobackend.security
|
||||||
|
|
||||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
||||||
|
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||||
|
import com.tarkvaratehnika.demobackend.web.rest.AuthenticationController
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.security.authentication.AuthenticationServiceException
|
import org.springframework.security.authentication.AuthenticationServiceException
|
||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.AuthenticationException
|
import org.springframework.security.core.AuthenticationException
|
||||||
@@ -37,11 +40,11 @@ import java.security.cert.X509Certificate
|
|||||||
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class AuthTokenDTOAuthenticationProvider {
|
object AuthTokenDTOAuthenticationProvider {
|
||||||
|
|
||||||
|
private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java)
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ROLE_USER : String = "ROLE_USER"
|
|
||||||
}
|
|
||||||
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +55,6 @@ class AuthTokenDTOAuthenticationProvider {
|
|||||||
val authentication = auth as PreAuthenticatedAuthenticationToken
|
val authentication = auth as PreAuthenticatedAuthenticationToken
|
||||||
val token = (authentication.credentials as AuthTokenDTO).token
|
val token = (authentication.credentials as AuthTokenDTO).token
|
||||||
val challenge = (authentication.credentials as AuthTokenDTO).challenge
|
val challenge = (authentication.credentials as AuthTokenDTO).challenge
|
||||||
|
|
||||||
val authorities = arrayListOf<GrantedAuthority>()
|
val authorities = arrayListOf<GrantedAuthority>()
|
||||||
authorities.add(USER_ROLE)
|
authorities.add(USER_ROLE)
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package com.tarkvaratehnika.demobackend.web
|
package com.tarkvaratehnika.demobackend.web
|
||||||
|
|
||||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER
|
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||||
import org.springframework.security.access.prepost.PreAuthorize
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.stereotype.Controller
|
import org.springframework.stereotype.Controller
|
||||||
import org.springframework.ui.Model
|
import org.springframework.ui.Model
|
||||||
|
@@ -26,7 +26,7 @@ class AuthenticationController {
|
|||||||
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
||||||
|
|
||||||
// Return authentication object if success.
|
// Return authentication object if success.
|
||||||
return AuthTokenDTOAuthenticationProvider().authenticate(auth)
|
return AuthTokenDTOAuthenticationProvider.authenticate(auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@@ -24,6 +24,7 @@ package com.tarkvaratehnika.demobackend.web.rest
|
|||||||
|
|
||||||
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
||||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
@@ -33,10 +34,12 @@ import org.webeid.security.nonce.NonceGenerator
|
|||||||
@RequestMapping("auth")
|
@RequestMapping("auth")
|
||||||
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||||
|
|
||||||
|
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
|
||||||
|
|
||||||
@GetMapping("challenge")
|
@GetMapping("challenge")
|
||||||
fun challenge(): ChallengeDto {
|
fun challenge(): ChallengeDto {
|
||||||
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
||||||
|
LOG.warn(challengeDto.nonce)
|
||||||
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
||||||
return challengeDto
|
return challengeDto
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
|
html {
|
||||||
|
font-size: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.cont {
|
.cont {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
@@ -11,7 +11,7 @@ function launchAuthApp(action) {
|
|||||||
httpGetAsync(originUrl + challengeUrl, (body) => {
|
httpGetAsync(originUrl + challengeUrl, (body) => {
|
||||||
let data = JSON.parse(body);
|
let data = JSON.parse(body);
|
||||||
let challenge = data.nonce;
|
let challenge = data.nonce;
|
||||||
let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling.
|
let intent = createParametrizedIntentUrl(challenge, action, originUrl); // TODO: Error handling.
|
||||||
console.log(intent);
|
console.log(intent);
|
||||||
window.location.href = intent;
|
window.location.href = intent;
|
||||||
pollForAuth(POLLING_INTERVAL, challenge);
|
pollForAuth(POLLING_INTERVAL, challenge);
|
||||||
@@ -20,8 +20,8 @@ function launchAuthApp(action) {
|
|||||||
|
|
||||||
function pollForAuth(timeout, challenge) {
|
function pollForAuth(timeout, challenge) {
|
||||||
console.log("Polling for auth");
|
console.log("Polling for auth");
|
||||||
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + challenge;
|
let encodedChallenge = encodeURIComponent(challenge);
|
||||||
|
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + encodedChallenge;
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
let timer = setInterval(() => {
|
let timer = setInterval(() => {
|
||||||
// Fetch authentication object.
|
// Fetch authentication object.
|
||||||
@@ -48,7 +48,7 @@ function createParametrizedIntentUrl(challenge, action) {
|
|||||||
else if (challenge == null) {
|
else if (challenge == null) {
|
||||||
console.error("Challenge missing, can't authenticate without it.")
|
console.error("Challenge missing, can't authenticate without it.")
|
||||||
} else {
|
} else {
|
||||||
return intentUrl + "?" + "action=" + action + "&challenge=" + challenge + "&authUrl=" + originUrl + authenticationRequestUrl;
|
return intentUrl + "?" + "action=" + action + "&challenge=" + encodeURIComponent(challenge) + "&authUrl=" + authenticationRequestUrl + "&originUrl=" + originUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-dark bg-dark">
|
<nav class="navbar navbar-dark bg-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
<a class="navbar-brand" href="#">Auth demo webapp</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="cont">
|
<div class="cont">
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
website using your ID card by using the button below.</h4>
|
website using your ID card by using the button below.</h4>
|
||||||
<h5>Make sure you've installed the authentication app from: <a
|
<h5>Make sure you've installed the authentication app from: <a
|
||||||
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">GitHub</a></h5>
|
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">GitHub</a></h5>
|
||||||
<button type="button" class="btn btn-secondary" id="loginButton" data-action="auth">Log in</button>
|
<button type="button" class="btn btn-lg btn-secondary" id="loginButton" data-action="auth">Log in</button>
|
||||||
<div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
|
<div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
|
||||||
Login failed. Refresh the page to try again.
|
Login failed. Refresh the page to try again.
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,38 +3,31 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Login</title>
|
<title>Login</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||||
<link th:href="@{/css/main.css}" rel="stylesheet">
|
<link th:href="@{/css/main.css}" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" th:src="@{/js/signature.js}"></script>
|
<script type="text/javascript" th:src="@{/js/signature.js}"></script>
|
||||||
<script type="text/javascript" th:src="@{/js/main.js}"></script>
|
<script type="text/javascript" th:src="@{/js/main.js}"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-dark bg-dark">
|
<nav class="navbar navbar-dark bg-dark">
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
<a class="navbar-brand" href="#">Auth demo web application</a>
|
||||||
</div>
|
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item active">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#">Log out<span class="sr-only">(current)</span></a>
|
<a href="/" class="btn btn-danger">Log out</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div class="cont">
|
<div class="cont">
|
||||||
<h4>Congratulations! You have just authenticated yourself using your mobile phone and your ID-card. You can try to give a signature to a file now.</h4>
|
<h4>Congratulations! You have just authenticated yourself using your mobile phone and your ID-card. You can try to
|
||||||
|
give a signature to a file now.</h4>
|
||||||
<h5>This page is still WIP, signing a document feature will be implemented later.</h5>
|
<h5>This page is still WIP, signing a document feature will be implemented later.</h5>
|
||||||
<div class="input-group mb-3">
|
|
||||||
<div class="custom-file">
|
<div class="custom-file">
|
||||||
<input type="file" class="custom-file-input" id="inputGroupFile01">
|
<input type="file" class="custom-file-input" id="customFile">
|
||||||
<label class="custom-file-label" for="inputGroupFile01">Choose file</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary" id="signFile" data-action="auth">Sign</button>
|
<button type="button" class="btn btn-secondary" id="signFile" data-action="auth">Sign</button>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user