46 Commits

Author SHA1 Message Date
Kevin
1cd86cc4f8 Merge pull request #18 from TanelOrumaa/Tests2
Moved UC4test to new branch
2021-12-07 10:47:52 +02:00
stargateprovider
634672db0f Moved UC4test to new branch 2021-12-07 10:36:27 +02:00
stargateprovider
be5c0568f8 Merge remote-tracking branch 'origin/Tests' into Tests 2021-12-07 00:40:15 +02:00
stargateprovider
b8c2da95a2 rebase 2021-12-07 00:39:21 +02:00
stargateprovider
624ebf3999 UC4 test 2021-12-07 00:39:21 +02:00
stargateprovider
7dde3239a0 Add invalidCAN test to UC4 test case and rebase 2021-12-07 00:39:21 +02:00
stargateprovider
0dfeb798e3 UC4 test 2021-12-07 00:39:21 +02:00
stargateprovider
371a871f87 update UC4Test 2021-12-07 00:38:32 +02:00
stargateprovider
8793ed9179 update test 2021-12-07 00:38:32 +02:00
stargateprovider
0c205eea8d Add invalidCAN test to UC4 test case and rebase 2021-12-07 00:38:32 +02:00
stargateprovider
8fe7aed941 UC4 test 2021-12-07 00:38:32 +02:00
stargateprovider
57de1bf979 Added basic notifications to the user
For when exceptions occur when communicating with the ID-card
2021-12-07 00:38:32 +02:00
stargateprovider
5c1f141405 rebase 2021-12-07 00:38:02 +02:00
stargateprovider
a61ea0b6cc UC4 test 2021-12-07 00:38:02 +02:00
stargateprovider
8d5a55c00e Add invalidCAN test to UC4 test case and rebase 2021-12-07 00:38:02 +02:00
stargateprovider
bf87eb1c07 UC4 test 2021-12-07 00:38:02 +02:00
stargateprovider
7e14bc289e Merge remote-tracking branch 'origin/Tests' into Tests
# Conflicts:
#	MobileAuthApp/app/src/androidTest/java/com/tarkvaraprojekt/mobileauthapp/UC4Test.kt
#	MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt
#	MobileAuthApp/app/src/main/res/values-en/strings.xml
#	MobileAuthApp/app/src/main/res/values-et/strings.xml
#	MobileAuthApp/app/src/main/res/values/strings.xml
2021-12-07 00:34:51 +02:00
stargateprovider
f274b48d68 update UC4Test 2021-12-07 00:34:15 +02:00
stargateprovider
515eea14bb update test 2021-12-07 00:01:42 +02:00
stargateprovider
e4a06b4fc9 Add invalidCAN test to UC4 test case and rebase 2021-12-07 00:01:42 +02:00
stargateprovider
24980b3253 UC4 test 2021-12-07 00:01:42 +02:00
stargateprovider
6ddfe9af46 Added basic notifications to the user
For when exceptions occur when communicating with the ID-card
2021-12-07 00:01:42 +02:00
Henrik Lepson
60207319b7 Merge pull request #16 from TanelOrumaa/newapproach
Improved code
2021-12-06 20:27:00 +02:00
stargateprovider
67ba0ed764 Merge remote-tracking branch 'origin/Tests' into Tests
# Conflicts:
#	MobileAuthApp/app/src/androidTest/java/com/tarkvaraprojekt/mobileauthapp/UC4Test.kt
#	MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt
#	MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java
2021-12-04 21:55:40 +02:00
stargateprovider
da2ba0b0da Add invalidCAN test to UC4 test case and rebase 2021-12-04 21:53:11 +02:00
stargateprovider
73b94adcd3 UC4 test 2021-12-04 21:51:57 +02:00
stargateprovider
339fa0a378 Added basic notifications to the user
For when exceptions occur when communicating with the ID-card
2021-12-04 21:51:57 +02:00
Henrik Lepson
e5300dfa5e got rid of git status syntax 2021-12-04 17:21:07 +02:00
Henrik Lepson
d4c2a11521 added more error messages 2021-12-04 17:08:58 +02:00
stargateprovider
0e15bede78 UC4 test 2021-12-04 14:28:52 +02:00
Henrik Lepson
09c4fa6be3 fixed small issue in testmobileapp 2021-12-04 12:46:40 +02:00
Henrik Lepson
63bc89b0e4 Merge pull request #14 from TanelOrumaa/iter4UI
UI/UX improvements for iteration 4
2021-12-03 16:24:00 +02:00
Henrik Lepson
152fd16162 MOB-40 finished home fragment 2021-12-03 16:11:48 +02:00
Henrik Lepson
716b983389 MOB-40 made user fragment scrollable, changed app logo 2021-11-29 20:22:20 +02:00
Henrik Lepson
94fad95364 MOB-40 added listening to NFC adapter changes 2021-11-28 16:54:26 +02:00
Henrik Lepson
c33fba1a14 MOB-40 pin toggle status saved, added informational snackbars 2021-11-28 16:24:08 +02:00
Henrik Lepson
825335ea5f MOB-40 added save can to home fragment, fixed language resources 2021-11-27 21:16:50 +02:00
Henrik Lepson
0f6f31c995 MOB-40 improved authentication UX 2021-11-25 18:09:45 +02:00
Henrik Lepson
762a8c8cc2 MOB-40 changed the settings view and fixed menu notifications 2021-11-25 16:13:35 +02:00
Henrik Lepson
1138abcb11 MOB-40 home fragment logic changed 2021-11-25 14:17:00 +02:00
Henrik Lepson
f085076631 MOB-40 improved CAN and PIN views 2021-11-24 20:44:40 +02:00
Henrik Lepson
edc444c027 MOB-40 added xml res files for styles and dimensions 2021-11-24 18:24:21 +02:00
Henrik Lepson
df5febabb7 deleted unused code 2021-11-24 18:20:22 +02:00
Henrik Lepson
1b9a59d4eb Removed irrelevant information from the readme
The main readme contained information that was not up-to-date. Mobile Auth App capabilities clarified as well.
2021-11-23 11:40:07 +02:00
TanelOrumaa
bbd5039a0b Merge pull request #11 from TanelOrumaa/MOB-42
Backend + frontend + MOB-21 JWT creation.
2021-11-17 09:58:06 +02:00
stargateprovider
73efb00368 Added basic notifications to the user
For when exceptions occur when communicating with the ID-card
2021-11-08 23:51:07 +02:00
36 changed files with 1066 additions and 904 deletions

View File

@@ -44,7 +44,9 @@ dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
debugImplementation 'androidx.fragment:fragment-testing:1.4.0'
//To use activityViewModels
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
@@ -68,8 +70,4 @@ dependencies {
'io.jsonwebtoken:jjwt-gson:0.11.2'
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'
}

View File

@@ -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()))
}
}

View File

@@ -1,13 +1,10 @@
package com.tarkvaraprojekt.mobileauthapp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.os.CountDownTimer
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -17,11 +14,14 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import java.io.IOException
import java.lang.Exception
import java.security.GeneralSecurityException
import kotlin.system.exitProcess
/**
@@ -69,7 +69,6 @@ class AuthFragment : Fragment() {
goToTheStart()
}
}.start()
//binding!!.nextButton.visibility = View.INVISIBLE
binding!!.nextButton.setOnClickListener { goToNextFragment() }
binding!!.cancelButton.setOnClickListener { goToTheStart() }
val adapter = NfcAdapter.getDefaultAdapter(activity)
@@ -78,24 +77,19 @@ class AuthFragment : Fragment() {
}
private fun getInfoFromIdCard(adapter: NfcAdapter) {
if (args.reading) {
adapter.enableReaderMode(activity, { tag ->
timer.cancel()
requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.card_detected)
}
var msgCode = 0
val card = IsoDep.get(tag)
card.timeout = 32768
card.use {
try {
val comms = Comms(it, viewModel.userCan)
if (args.auth) {
val jws = Authenticator(comms).authenticate(
intentParameters.challenge,
intentParameters.origin,
viewModel.userPin
)
intentParameters.setToken(jws)
} else {
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
viewModel.setUserFirstName(response[1])
viewModel.setUserLastName(response[0])
@@ -103,24 +97,45 @@ class AuthFragment : Fragment() {
viewModel.setGender(response[3])
viewModel.setCitizenship(response[4])
viewModel.setExpiration(response[5])
}
requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.data_read)
}
} catch (e: Exception) {
requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.no_success)
}
} catch (e: android.nfc.TagLostException) {
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.
viewModel.deleteCan(requireContext())
// Gives user some time to read the error message
Thread.sleep(1000)
goToTheStart()
} catch (e: AuthAppException) {
msgCode = when (e.code) {
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 {
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)
} else { //We want to create a JWT instead of reading the info from the card.
goToNextFragment()
}
}
private fun goToNextFragment() {
@@ -141,7 +156,8 @@ class AuthFragment : Fragment() {
} else {
if (!args.mobile) {
//Currently for some reason the activity is not killed entirely. Must be looked into further.
requireActivity().finishAndRemoveTask()
requireActivity().finish()
exitProcess(0)
} else {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)

View File

@@ -3,17 +3,22 @@ package com.tarkvaraprojekt.mobileauthapp
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import org.w3c.dom.Text
/**
* Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved
@@ -24,12 +29,11 @@ class CanFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentCanBinding? = null
private var _binding: FragmentCanBinding? = null
private val binding get() = _binding!!
// Navigation arguments:
// saving = true means that we are navigating here from the settings menu and must return to the settings menu.
// 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()
override fun onCreateView(
@@ -37,20 +41,17 @@ class CanFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentCanBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentCanBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkIfSkip()
// If the user arrives from the settings menu then the button should say
// save instead of continue.
if (args.saving) {
binding!!.nextButton.text = getString(R.string.save_text)
binding.canTextField.editText?.addTextChangedListener {
checkEnteredCan()
}
binding!!.nextButton.setOnClickListener { checkEnteredCan() }
binding!!.cancelButton.setOnClickListener { goToTheStart() }
binding.buttonCancel.setOnClickListener { goToTheStart() }
}
/**
@@ -67,66 +68,22 @@ class CanFragment : Fragment() {
* Takes user to the next fragment, which is PinFragment.
*/
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)
}
/**
* 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.
* If the user arrived from the settings menu then the start is the settings menu
* not the HomeFragment.
*/
private fun goToTheStart() {
// TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment.
if (args.saving) {
if (args.fromhome) {
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
} else {
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
}
} else if (args.auth || args.mobile) {
if (args.mobile) {
val resultIntent = Intent()
@@ -140,8 +97,37 @@ class CanFragment : Fragment() {
}
}
/**
* Method that creates and shows a snackbar that tells the user that CAN has been saved
*/
private fun showSnackbar() {
val snackbar = Snackbar.make(requireView(), R.string.can_status_saved, Snackbar.LENGTH_SHORT)
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
snackbar.show()
}
/**
* Checks whether the user has entered a 6 digit can to the input field.
* If yes then the user is allowed to continue otherwise the user is
* allowed to modify the entered can.
*/
private fun checkEnteredCan() {
val enteredCan = binding.canTextField.editText?.text.toString()
if (enteredCan.length == 6) {
viewModel.setUserCan(enteredCan)
viewModel.storeCan(requireContext()) //Maybe storeCan should always automatically call setUserCan method as well because these methods usually are used together
showSnackbar()
if (args.saving) {
goToTheStart()
} else {
goToTheNextFragment()
}
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -1,20 +1,30 @@
package com.tarkvaraprojekt.mobileauthapp
import android.content.BroadcastReceiver
import android.content.Context
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.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import java.lang.Exception
import java.net.URLDecoder
/**
* HomeFragment is only shown to the user when then the user launches the application. When the application
@@ -29,33 +39,68 @@ class HomeFragment : Fragment() {
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(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
// Making settings menu active again
(activity as MainActivity).menuAvailable = true
return binding!!.root
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialChecks()
var auth = false
if (requireActivity().intent.data?.getQueryParameter("action") != null) {
// Currently we only support authentication not signing.
auth = true
}
val mobile = requireActivity().intent.getBooleanExtra("mobile", false)
if (auth || mobile) {
startAuthentication(mobile)
} else {
receiver = object : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
updateAction(canSaved)
}
}
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
requireActivity().registerReceiver(receiver, filter)
updateAction(canSaved)
}
}
/**
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
*/
private fun goToTheNextFragment(mobile: Boolean = false) {
(activity as MainActivity).menuAvailable = false
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
findNavController().navigate(action)
}
/**
* Method that starts the authentication use case.
*
* NOTE: Comment out try-catch block when testing without backend
*/
private fun startAuthentication(mobile: Boolean) {
try {
if (mobile) {
// We use !! because we want an exception when something is not right.
// 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")!!)
@@ -67,41 +112,56 @@ class HomeFragment : Fragment() {
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 {
message.setMessage(getString(R.string.problem_other))
}
message.setPositiveButton(getString(R.string.continue_button)) {_, _ ->
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
goToTheNextFragment(true, mobile)
message.show()
}
binding!!.beginButton.setOnClickListener { goToTheNextFragment() }
}
/**
* Method where all the initial checks that should be done before any user input is accepted should be added.
* Checks the state of the CAN, saved or not saved. Updates the text and logo.
*/
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.
*/
private fun goToTheNextFragment(auth: Boolean = false, mobile: Boolean = false) {
// Making settings menu inactive
(activity as MainActivity).menuAvailable = false
// Currently saving is true because the application is not yet integrated with
// 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)
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 {
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(reading = true, auth = false, mobile = mobile)
findNavController().navigate(action)
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)
}
}
@@ -116,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() {
if (viewModel.userCan.length == 6) {
binding!!.canStatusText.text = getString(R.string.can_status_saved)
binding!!.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
private fun initialChecks() {
viewModel.checkCan(requireContext())
viewModel.checkPin(requireContext())
displayStates()
}
/**
* Displays a help message to the user explaining what the CAN is
*/
private fun displayMessage(title: String, message: String) {
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.return_text){_, _ -> }
.show()
val title = dialog.findViewById<TextView>(R.id.alertTitle)
title?.textSize = 24F
}
/**
* Informs user whether the ID card can be detected or not.
*/
private fun updateAction(canIsSaved: Boolean) {
if (canIsSaved) {
binding.detectionActionText.text = getString(R.string.action_detect)
enableReaderMode()
binding.homeActionButton.visibility = View.GONE
binding.homeHelpButton.visibility = View.GONE
} else {
binding!!.canStatusText.text = getString(R.string.can_status_negative)
binding!!.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
binding.detectionActionText.text = getString(R.string.action_detect_unavailable)
binding.homeActionButton.text = getString(R.string.add_can_text)
binding.homeActionButton.setOnClickListener {
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(saving = true, fromhome = true)
findNavController().navigate(action)
}
binding.homeHelpButton.setOnClickListener {
displayMessage(getString(R.string.can_question), getString(R.string.can_explanation))
}
binding.homeActionButton.visibility = View.VISIBLE
binding.homeHelpButton.visibility = View.VISIBLE
}
}
/**
* 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() {
if (viewModel.userPin.length in 4..12) {
binding!!.pinStatusText.text = getString(R.string.pin_status_saved)
binding!!.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
private fun reset() {
binding.homeActionButton.text = getString(R.string.try_again_text)
binding.homeActionButton.setOnClickListener {
updateAction(canSaved)
}
binding.homeActionButton.visibility = View.VISIBLE
}
/**
* Method that enables the NFC reader mode, which allows the app to communicate with the ID card and retrieve information.
*/
private fun enableReaderMode() {
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter == null || !adapter.isEnabled) {
binding.detectionActionText.text = getString(R.string.nfc_not_available)
} else {
binding!!.pinStatusText.text = getString(R.string.pin_status_negative)
binding!!.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
adapter.enableReaderMode(activity, { tag ->
requireActivity().runOnUiThread {
binding.detectionActionText.text = getString(R.string.card_detected)
}
val card = IsoDep.get(tag)
card.timeout = 32768
card.use {
try {
val comms = Comms(it, viewModel.userCan)
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
viewModel.setUserFirstName(response[1])
viewModel.setUserLastName(response[0])
viewModel.setUserIdentificationNumber(response[2])
viewModel.setGender(response[3])
viewModel.setCitizenship(response[4])
viewModel.setExpiration(response[5])
requireActivity().runOnUiThread {
val action = HomeFragmentDirections.actionHomeFragmentToUserFragment()
findNavController().navigate(action)
}
} catch (e: Exception) {
when(e) {
is TagLostException -> requireActivity().runOnUiThread {
binding.detectionActionText.text = getString(R.string.id_card_removed_early)
reset()
}
else -> requireActivity().runOnUiThread {
binding.detectionActionText.text = getString(R.string.nfc_reading_error)
viewModel.deleteCan(requireContext())
canState()
reset()
}
}
} finally {
adapter.disableReaderMode(activity)
}
}
}, NfcAdapter.FLAG_READER_NFC_A, null)
}
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
if (receiver != null) {
requireActivity().unregisterReceiver(receiver)
}
_binding = null
}
}

View File

@@ -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.
var menuAvailable: Boolean = true
var inMenu: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
@@ -40,9 +42,13 @@ class MainActivity : AppCompatActivity() {
R.id.menu_settings_option -> {
if (menuAvailable) {
navigationController.navigate(R.id.action_homeFragment_to_settingsFragment)
menuAvailable = false
inMenu = true
true
} 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
}
}

View File

@@ -3,6 +3,9 @@ package com.tarkvaraprojekt.mobileauthapp.NFC;
import android.nfc.tech.IsoDep;
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.engines.AESEngine;
import org.bouncycastle.crypto.macs.CMac;
@@ -21,7 +24,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -31,43 +33,47 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Comms {
private static final byte[] master = { // select Main AID
0, -92, 4, 12, 16, -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0
};
private static final byte[] 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 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 IsoDep idCard;
private final byte[] keyEnc;
private final byte[] keyMAC;
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 {
idCard.connect();
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];
keyMAC = keys[1];
}
public byte[] getAuthenticationCertificate() {
return new byte[0];
}
/**
* Calculates the message authentication code
*
@@ -139,51 +154,40 @@ public class Comms {
* @param CAN the card access number provided by the user
* @return the decrypted nonce
*/
private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
byte[] decryptionKey = createKey(CAN, (byte) 3);
private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
return cipher.doFinal(encryptedNonce);
}
/**
* Communicates with the card and logs the response
*
* @param APDU The command
* @param log Information for logging
* @return The response
*/
private byte[] getResponse(byte[] APDU, String log) throws IOException {
byte[] response = idCard.transceive(APDU);
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException(String.format("%s failed.", log));
}
Log.i(log, Hex.toHexString(response));
return response;
}
/**
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
*
* @param CAN the card access number
*/
private byte[][] PACE(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
getResponse(selectMaster, "Select the master application");
// select the ECC applet on the chip
byte[] response = idCard.transceive(master);
Log.i("Select applet", Hex.toHexString(response));
// initiate PACE
getResponse(MSESetAT, "Set authentication template");
response = idCard.transceive(MSESetAT);
Log.i("Authentication template", Hex.toHexString(response));
// 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);
// generate an EC keypair and exchange public keys with the chip
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
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));
// 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();
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
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));
// generate the session keys and exchange MACs to verify them
byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
byte[] keyEnc = createKey(secret, (byte) 1);
byte[] keyMAC = createKey(secret, (byte) 2);
byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
sharedSecret = cardPublicKey.multiply(privateKey).normalize();
byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded();
byte[] keyEnc = createKey(encodedSecret, (byte) 1);
byte[] keyMAC = createKey(encodedSecret, (byte) 2);
APDU = createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65);
byte[] MAC = getMAC(APDU, keyMAC);
APDU = createAPDU(GAMutualAuthenticationIncomplete, MAC, 9);
response = idCard.transceive(APDU);
Log.i("Mutual authentication", Hex.toHexString(response));
// verify chip's MAC and return session keys
MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
// if the chip-side verification fails, crash and burn
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))) {
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};
}
/**
* 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
*
@@ -260,182 +258,74 @@ public class Comms {
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
macData[15] = ssc; // first block contains the ssc
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
macData[20] = (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
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
byte[] paddedData = Arrays.copyOf(data, length);
paddedData[data.length] = (byte) 0x80;
paddedData[data.length] = -128;
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
}
macData[35 + encryptedData.length] = (byte) 0x80;
macData[35 + encryptedData.length] = -128;
byte[] MAC = getMAC(macData, keyMAC);
// 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) {
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);
ssc++;
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
*
* @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16)
* @return array containing the corresponding data strings
* @param FID the last bytes of file identifiers being requested
* @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];
int stringIndex = 0;
// select the master application
selectFile(IASECCFID, "the master application");
String[] personalData = new String[FID.length];
byte[] data;
byte[] APDU;
byte[] response;
// 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 personal data elementary files
for (byte index : lastBytes) {
// select and read the first 8 elementary files in the DF
for (int i = 0; i < FID.length; i++) {
if (index > 15 || index < 1) throw new RuntimeException("Invalid personal data FID.");
FID[1] = index;
byte index = FID[i];
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
byte[] response = readFile(FID, "a personal data EF");
int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2;
personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator));
byte[] raw = encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(raw).lastIndexOf("80") / 2;
personalData[i] = new String(Arrays.copyOfRange(raw, 0, indexOfTerminator));
}
return personalData;
}
/**
* Attempts to verify the selected PIN
*
* @param PIN user-provided PIN
* @param oneOrTwo true for PIN1, false for PIN2
*/
private void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
selectFile(IASECCFID, "the master application");
if (!oneOrTwo) {
selectFile(QSCD, "the application");
}
// pad the PIN and use the chip for verification
byte[] paddedPIN = Hex.decode("ffffffffffffffffffffffff");
System.arraycopy(PIN, 0, paddedPIN, 0, PIN.length);
byte[] response = getResponse(paddedPIN, oneOrTwo ? verifyPIN1 : verifyPIN2, "PIN verification");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
if (response[response.length - 2] == 0x69 && response[response.length - 1] == (byte) 0x83) {
throw new RuntimeException("Invalid PIN. Authentication method blocked.");
} else {
throw new RuntimeException(String.format("Invalid PIN. Attempts left: %d.", response[response.length - 1] + 64));
}
}
}
/**
* Retrieves the authentication or signature certificate from the chip
*
* @param authOrSign true for auth, false for sign cert
* @return the requested certificate
*/
public byte[] getCertificate(boolean authOrSign) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
selectFile(IASECCFID, "the master application");
selectFile(authOrSign ? AWP : QSCD, "the application");
selectFile(authOrSign ? authCert : signCert, "the certificate");
byte[] certificate = new byte[0];
byte[] readCert = Arrays.copyOf(readFile, readFile.length);
// Construct the certificate byte array n=indexOfTerminator bytes at a time
for (int i = 0; i < 16; i++) {
// Set the P1/P2 values to incrementally read the certificate
readCert[2] = (byte) (certificate.length / 256);
readCert[3] = (byte) (certificate.length % 256);
byte[] response = getResponse(new byte[0], readCert, "Read the certificate");
if (response[response.length - 2] == 0x6b && response[response.length - 1] == 0x00) {
throw new RuntimeException("Wrong read parameters.");
}
// Set the range containing a portion of the certificate and decrypt it
int start = response[2] == 1 ? 3 : 4;
int end = start + (response[start - 2] + 256) % 256 - 1;
byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, start, end), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2;
certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator);
System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator);
if (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == 0x00) {
break;
}
}
return certificate;
}
/**
* Signs the authentication token hash
*
* @param PIN1 PIN1
* @param token the token hash to be signed
* @return authentication token hash signature
*/
public byte[] authenticate(String PIN1, byte[] token) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
verifyPIN(PIN1.getBytes(StandardCharsets.UTF_8), true);
selectFile(AWP, "the AWP application");
byte[] response = getResponse(Env, MSESetEnv, "Set environment");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException("Setting the environment failed.");
}
InternalAuthenticate[4] = (byte) (0x1d + 16 * (token.length / 16));
InternalAuthenticate[6] = (byte) (0x11 + 16 * (token.length / 16));
response = getResponse(token, InternalAuthenticate, "Internal Authenticate");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException("Signing the token failed.");
}
byte[] signature = encryptDecryptData(Arrays.copyOfRange(response, 3, 115), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(signature).lastIndexOf("80") / 2;
return Arrays.copyOf(signature, indexOfTerminator);
}
private byte[] getResponse(byte[] data, byte[] command, String log) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
byte[] response = idCard.transceive(createSecureAPDU(data, command));
Log.i(log, Hex.toHexString(response));
return response;
}
}

View File

@@ -19,23 +19,24 @@ class Pin2Fragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentPin2Binding? = null
private var _binding: FragmentPin2Binding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentPin2Binding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentPin2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.nextButton.setOnClickListener {
binding.nextButton.setOnClickListener {
checkPin2Length()
}
binding!!.cancelButton.setOnClickListener {
binding.cancelButton.setOnClickListener {
cancel()
}
}
@@ -45,7 +46,7 @@ class Pin2Fragment : Fragment() {
* then it is saved to the viewModel.
*/
private fun checkPin2Length() {
val enteredPin2 = binding!!.pin2EditText.editText?.text.toString()
val enteredPin2 = binding.pin2EditText.editText?.text.toString()
if (enteredPin2.length in 5..12) {
viewModel.setUserPin2(enteredPin2)
} else {
@@ -66,7 +67,7 @@ class Pin2Fragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -1,17 +1,22 @@
package com.tarkvaraprojekt.mobileauthapp
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
@@ -24,110 +29,67 @@ class PinFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentPinBinding? = null
private var _binding: FragmentPinBinding? = null
private val binding get() = _binding!!
// Navigation arguments:
// saving = true means that the user must be returned to the settings menu
// 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 var saveToggle = true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentPinBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentPinBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkIfSkip()
// If the user arrives from the settings menu then the button says
// save instead of continue.
// Switch should be not visible when user is in savings mode
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!!.cancelButton.setOnClickListener { goToTheStart() }
binding.saveSwitch.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
binding.saveStatus.text = getString(R.string.pin_save_on)
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", true)?.apply()
} else {
binding.saveStatus.text = getString(R.string.pin_save_off)
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", false)?.apply()
}
/**
* 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()
saveToggle = !saveToggle
}
}
binding.buttonContinue.setOnClickListener { checkEnteredPin() }
binding.buttonCancel.setOnClickListener { goToTheStart() }
}
/**
* Takes user to the next fragment, which is AuthFragment.
*/
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)
}
/**
* 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
* settings menu not the HomeFragment.
*/
private fun goToTheStart() {
if (args.saving) {
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
findNavController().navigate(R.id.action_pinFragment_to_settingsFragment)
} else if (args.auth || args.mobile) {
if (args.mobile) {
val resultIntent = Intent()
@@ -141,8 +103,55 @@ class PinFragment : Fragment() {
}
}
/**
* Checks if the current fragment can be skipped or not.
* If the user has PIN 1 saved on the device or PIN 1 is not required
* then the PIN 1 won't be asked.
*/
private fun checkIfSkip() {
if (viewModel.userPin.length in 4..12) {
goToTheNextFragment()
}
}
/**
* Method that creates and shows a snackbar that tells the user that PIN 1 has been saved
*/
private fun showSnackbar() {
val snackbar = Snackbar.make(requireView(), R.string.pin_status_saved, Snackbar.LENGTH_SHORT)
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
snackbar.show()
}
/**
* Checks whether the user has entered a PIN 1 with length between [4, 12] in the
* input field. If yes then the user is allowed to continue otherwise the user is
* allowed to modify the entered PIN 1.
*/
private fun checkEnteredPin() {
val enteredPin = binding.pinTextField.editText?.text.toString()
if (enteredPin.length in 4..12) {
viewModel.setUserPin(enteredPin)
if (args.saving) {
viewModel.storePin(requireContext())
showSnackbar()
goToTheStart()
} else {
if (saveToggle) {
viewModel.storePin(requireContext())
showSnackbar()
}
goToTheNextFragment()
}
} else {
Toast.makeText(requireContext(), getString(R.string.pin_helper_text), Toast.LENGTH_SHORT)
.show()
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -24,7 +24,8 @@ class ResultFragment : Fragment() {
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()
@@ -33,16 +34,27 @@ class ResultFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentResultBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentResultBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.resultBackButton.visibility = View.GONE
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()
}
/**
* Makes a POST request to the backend server with a tokenItem
*/
@@ -57,17 +69,13 @@ class ResultFragment : Fragment() {
.setJsonObjectBody(json)
.asJsonObject()
.setCallback { e, result ->
// do stuff with the result or error
if (result == null) {
// TODO: Set auth message failed and close the app
Log.i("Log thingy fail", "result was null")
if (args.mobile) {
createResponse(false)
} else {
requireActivity().finishAndRemoveTask()
}
} else {
Log.i("POST request response", result.toString())
if (args.mobile) {
createResponse(true, result.toString(), paramsModel.token)
} else {
@@ -77,21 +85,9 @@ class ResultFragment : Fragment() {
}
}
/**
* 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()
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,17 @@
package com.tarkvaraprojekt.mobileauthapp.menu
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import com.tarkvaraprojekt.mobileauthapp.MainActivity
import com.tarkvaraprojekt.mobileauthapp.R
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentSettingsBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
@@ -21,7 +25,8 @@ class SettingsFragment : Fragment() {
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
@@ -30,8 +35,8 @@ class SettingsFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -39,10 +44,20 @@ class SettingsFragment : Fragment() {
showCanField()
showPinField()
togglePinButton()
binding!!.canMenuAction.setOnClickListener { canAction() }
binding!!.pinMenuAction.setOnClickListener { pinAction() }
binding!!.pinMenuShow.setOnClickListener { togglePin() }
binding!!.returnButton.setOnClickListener { backToHome() }
binding.canMenuAction.setOnClickListener { canAction() }
binding.pinMenuAction.setOnClickListener { pinAction() }
binding.pinMenuShow.setOnClickListener { togglePin() }
binding.returnButton.setOnClickListener { backToHome() }
}
/**
* Method for showing a snackbar with a message that is given as a parameter
*/
private fun showSnackbar(message: String) {
val snackbar = Snackbar.make(requireView(), message, Snackbar.LENGTH_SHORT)
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
snackbar.show()
}
/**
@@ -50,11 +65,11 @@ class SettingsFragment : Fragment() {
*/
private fun showCanField() {
if (viewModel.userCan.length == 6) {
binding!!.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
binding!!.canMenuAction.text = getString(R.string.can_delete)
binding.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
binding.canMenuAction.text = getString(R.string.can_delete)
} else {
binding!!.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
binding!!.canMenuAction.text = getString(R.string.can_add)
binding.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
binding.canMenuAction.text = getString(R.string.add_can_text)
}
}
@@ -66,7 +81,9 @@ class SettingsFragment : Fragment() {
if (viewModel.userCan.length == 6) {
viewModel.deleteCan(requireContext())
showCanField()
showSnackbar(getString(R.string.can_deleted))
} else {
(activity as MainActivity).inMenu = false
val action = SettingsFragmentDirections.actionSettingsFragmentToCanFragment(saving = true)
findNavController().navigate(action)
}
@@ -79,16 +96,16 @@ class SettingsFragment : Fragment() {
*/
private fun showPinField() {
if (viewModel.userPin.length in 4..12) {
binding!!.pinMenuShow.visibility = Button.VISIBLE
binding.pinMenuShow.visibility = Button.VISIBLE
if (showPin)
binding!!.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
binding.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
else
binding!!.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
binding!!.pinMenuAction.text = getString(R.string.pin1_delete)
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
binding.pinMenuAction.text = getString(R.string.pin1_delete)
} else {
binding!!.pinMenuShow.visibility = Button.GONE
binding!!.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
binding!!.pinMenuAction.text = getString(R.string.pin1_add)
binding.pinMenuShow.visibility = Button.GONE
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
binding.pinMenuAction.text = getString(R.string.pin1_add)
}
}
@@ -100,7 +117,9 @@ class SettingsFragment : Fragment() {
if (viewModel.userPin.length in 4..12) {
viewModel.deletePin(requireContext())
showPinField()
showSnackbar(getString(R.string.pin_deleted))
} else {
(activity as MainActivity).inMenu = false
val action = SettingsFragmentDirections.actionSettingsFragmentToPinFragment(saving = true)
findNavController().navigate(action)
}
@@ -120,9 +139,9 @@ class SettingsFragment : Fragment() {
*/
private fun togglePinButton() {
if (showPin) {
binding!!.pinMenuShow.text = getString(R.string.hide)
binding.pinMenuShow.text = getString(R.string.hide)
} 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.
*/
private fun backToHome() {
(activity as MainActivity).inMenu = false
findNavController().navigate(R.id.action_settingsFragment_to_homeFragment)
}
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

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

View File

@@ -1,9 +0,0 @@
package com.tarkvaraprojekt.mobileauthapp.network
/**
* TokenItem for making POST request.
*/
data class TokenItem (
val token: String,
val challenge: String,
)

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
android:padding="@dimen/padding"
tools:context=".HomeFragment">
<LinearLayout
@@ -20,7 +20,7 @@
android:id="@+id/can_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:layout_margin="@dimen/margin_big"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
@@ -32,14 +32,14 @@
<ImageView
android:id="@+id/can_status_logo"
android:layout_marginStart="12dp"
android:layout_marginStart="@dimen/margin"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/can_status_text"
android:textSize="20sp"
android:padding="12dp"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
@@ -51,7 +51,7 @@
android:id="@+id/pin_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:layout_margin="@dimen/margin_big"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
@@ -63,14 +63,14 @@
<ImageView
android:id="@+id/pin_status_logo"
android:layout_marginStart="12dp"
android:layout_marginStart="@dimen/margin"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/pin_status_text"
android:textSize="20sp"
android:padding="12dp"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
@@ -80,15 +80,49 @@
</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
android:id="@+id/begin_button"
android:id="@+id/home_action_button"
android:textSize="@dimen/regular_text"
android:text="@string/try_again_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/begin_text"
android:layout_marginTop="24dp"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/saved_states"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_huge"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
app:layout_constraintTop_toBottomOf="@id/id_card_detection"/>
<Button
android:id="@+id/home_help_button"
android:textSize="@dimen/regular_text"
android:text="@string/help_text"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_huge"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="?attr/materialButtonOutlinedStyle"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/home_action_button"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -4,81 +4,111 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".PinFragment">
<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">
android:padding="@dimen/padding"
tools:context=".MainActivity">
<TextView
android:id="@+id/pin_fragment_text"
android:layout_width="wrap_content"
android:id="@+id/title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:text="@string/pin_fragment" />
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
android:text="@string/pin_view"
android:textSize="@dimen/headline_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pin_edit_text"
android:id="@+id/pinTextField"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:hint="@string/enter_pin"
android:layout_margin="@dimen/margin_big"
android:hint="@string/hint_pin"
app:counterEnabled="true"
app:counterMaxLength="12"
app:counterOverflowTextAppearance="@style/helper"
app:counterTextAppearance="@style/helper"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:helperText="@string/example_pin"
app:helperText="@string/pin_helper_text"
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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:inputType="numberPassword"
android:textSize="14sp" />
android:singleLine="true"
android:textSize="@dimen/regular_text" />
</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>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/next_button"
android:id="@+id/button_continue"
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:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/next_button"
android:layout_margin="@dimen/margin_big"
android:fontFamily="sans-serif"
android:text="@string/continue_button"
android:textSize="@dimen/regular_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/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>

View File

@@ -64,7 +64,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/next_text"
android:text="@string/continue_button"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"

View File

@@ -4,14 +4,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
android:padding="@dimen/padding"
tools:context=".ResultFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/can_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:layout_margin="@dimen/margin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -27,33 +27,23 @@
<TextView
android:id="@+id/result_text"
android:text="@string/result_text"
android:textSize="20sp"
android:padding="12dp"
android:layout_marginVertical="6dp"
android:textSize="@dimen/regular_text"
android:padding="@dimen/padding_small"
android:layout_marginVertical="@dimen/margin_small"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/result_info_text"
android:text="@string/result_info"
android:padding="12dp"
android:textSize="16sp"
android:layout_marginVertical="6dp"
android:padding="@dimen/padding_small"
android:textSize="@dimen/regular_text"
android:layout_marginVertical="@dimen/margin_small"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<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>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="12dp"
android:layout_margin="@dimen/padding"
tools:context=".menu.SettingsFragment">
<com.google.android.material.card.MaterialCardView
@@ -22,44 +22,46 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
android:padding="@dimen/padding_small">
<TextView
android:id="@+id/can_saved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:padding="12dp"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin_small"
android:text="@string/saved_can" />
<Button
android:id="@+id/can_menu_action"
android:layout_margin="12dp"
android:textSize="15sp"
android:layout_margin="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/pin_saved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:padding="12dp"
android:textSize="@dimen/regular_text"
android:padding="@dimen/margin_small"
android:text="@string/saved_pin"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/pin_menu_action"
android:layout_margin="12dp"
android:textSize="15sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:orientation="vertical">
<Button
android:id="@+id/pin_menu_show"
android:layout_margin="12dp"
android:textSize="15sp"
android:layout_marginHorizontal="@dimen/margin"
android:layout_marginVertical="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
<Button
android:id="@+id/pin_menu_action"
android:layout_marginHorizontal="@dimen/margin"
android:layout_marginVertical="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -69,8 +71,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/return_text"
android:layout_margin="24dp"
android:textSize="15sp"
android:layout_marginVertical="@dimen/margin"
android:layout_marginStart="@dimen/padding"
android:textSize="@dimen/regular_text"
app:layout_constraintTop_toBottomOf="@id/settings_card"
app:layout_constraintStart_toStartOf="parent" />

View File

@@ -1,17 +1,20 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
android:padding="@dimen/padding"
tools:context=".UserFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_margin="@dimen/margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -23,86 +26,86 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20sp">
android:padding="@dimen/padding_tiny">
<TextView
android:id="@+id/user_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/user_name_label"
android:textSize="14sp" />
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/user_name"
android:textSize="20sp"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
<TextView
android:id="@+id/identification_number_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/identification_number_label"
android:textSize="14sp" />
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/identification_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
<TextView
android:id="@+id/gender_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/gender_label"
android:textSize="14sp" />
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
<TextView
android:id="@+id/expiration_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/expiration_label"
android:textSize="14sp" />
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/expiration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
<TextView
android:id="@+id/citizenship_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/citizenship_label"
android:textSize="14sp" />
android:textSize="@dimen/regular_text" />
<TextView
android:id="@+id/citizenship"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:layout_marginTop="@dimen/margin_small"
android:textSize="@dimen/regular_text"
android:textStyle="bold" />
</LinearLayout>
@@ -113,11 +116,12 @@
android:id="@+id/clear_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/clear_button"
android:textSize="18sp"
android:layout_marginTop="@dimen/margin_big"
android:text="@string/return_text"
android:textSize="@dimen/regular_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

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

View File

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

View File

@@ -18,6 +18,9 @@
android:id="@+id/action_homeFragment_to_canFragment"
app:destination="@id/canFragment"
app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_homeFragment_to_userFragment"
app:destination="@id/userFragment" />
</fragment>
<fragment
android:id="@+id/pinFragment"
@@ -42,10 +45,6 @@
android:id="@+id/action_pinFragment_to_authFragment"
app:destination="@id/authFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="reading"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="auth"
app:argType="boolean"
@@ -78,10 +77,6 @@
android:id="@+id/action_canFragment_to_pinFragment"
app:destination="@id/pinFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="reading"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="auth"
app:argType="boolean"
@@ -90,6 +85,10 @@
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="fromhome"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/authFragment"
@@ -113,10 +112,6 @@
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="reading"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"

View File

@@ -44,7 +44,9 @@
<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="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 name="user_name_label">NAME</string>
@@ -75,4 +77,7 @@
<string name="hidden_pin">****</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="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>

View File

@@ -43,7 +43,9 @@
<string name="auth_instruction_text">ID kaardiga ühenduse loomiseks pane kaart vastu telefoni</string>
<string name="time_left">Aega on jäänud %d sek</string>
<string name="no_time">Aeg on otsas</string>
<string name="no_success">Vale CAN</string>
<string name="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 name="user_name_label">NIMI</string>
@@ -73,4 +75,7 @@
<string name="hide">PEIDA</string>
<string name="hidden_pin">****</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>

View File

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

View File

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

View File

@@ -43,7 +43,9 @@
<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="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 name="user_name_label">NAME</string>
@@ -73,4 +75,7 @@
<string name="hide">HIDE</string>
<string name="hidden_pin">****</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>

View File

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

View File

@@ -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 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
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.
@@ -22,12 +20,13 @@ More info about installing third party applications on the Android phones: https
### 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
* [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision) *last updated on 10.10*
* [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes) *last updated for iteration3 on 08.11*
* [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)
* [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*
* [Use Cases](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Cases) *last updated on 10.10*
* [User stories](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/User-stories) *last updated on 10.10*
* [Use Case Tests](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Case-Tests) *WIP*
* [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)
* [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)

View File

@@ -9,7 +9,6 @@ import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.example.testmobileapp.databinding.ActivityMainBinding
import com.google.gson.JsonObject
import com.koushikdutta.ion.Ion
import org.json.JSONObject
@@ -35,9 +34,8 @@ class MainActivity : AppCompatActivity() {
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
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)
// 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 = ""
@@ -48,14 +46,6 @@ class MainActivity : AppCompatActivity() {
Log.i("getResult", "unable to retrieve name from principal")
}
showResult(user)
/*
binding.loginOptionNfcButton.text = "Log Out"
binding.loginOptionNfcButton.setOnClickListener {
binding.loginOptionNfcButton.text = "NFC auth"
binding.loginOptionNfcButton.setOnClickListener { getData() }
}
*/
}
if (response.resultCode == Activity.RESULT_CANCELED) {
binding.loginTextView.text = getString(R.string.auth_failure)
@@ -114,6 +104,7 @@ class MainActivity : AppCompatActivity() {
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