40 Commits

Author SHA1 Message Date
TanelOrumaa
9b0cb1a22d MOB-42 Changed POST request logic 2021-11-08 23:52:38 +02:00
TanelOrumaa
1d665f02bf MOB-42 Added JWT emitting 2021-11-08 23:17:36 +02:00
TanelOrumaa
82b1538867 MOB-42 Fixed JWT generation issue. 2021-11-08 22:41:09 +02:00
Henrik Lepson
c51f0e4277 Release notes updated 2021-11-08 21:51:54 +02:00
Henrik Lepson
aaa8d8f13c Merge pull request #12 from TanelOrumaa/fixesIter3
Iter3 related fixes
2021-11-08 20:36:12 +02:00
Henrik Lepson
d60cecdd26 forgot to remove dummy values 2021-11-08 20:26:17 +02:00
Henrik Lepson
f0c7ab96bb MOB-23 MOB-41 small fixes related to HTTP 2021-11-08 20:22:33 +02:00
Henrik Lepson
1b79eba4a4 MOB-23 setup for post request done, parameter viewModel added 2021-11-08 19:07:30 +02:00
TanelOrumaa
c46c3082b7 Merged main to branch to support DeepLink 2021-11-08 18:02:56 +02:00
TanelOrumaa
e4a9a4da1b Added readme to backend demo project 2021-11-08 17:49:42 +02:00
TanelOrumaa
f9cd30922e MOB-42 Added backend server, two frontend webpages and rest endpoints for getting challenge, submitting authentication token and getting authentication object. MOB-21 Added JWT creation, but whole process still needs some work. 2021-11-08 17:30:56 +02:00
Henrik Lepson
4252e3e637 Merge pull request #10 from TanelOrumaa/authprep
Authprep
2021-11-07 14:35:52 +02:00
Henrik Lepson
64357ca1d3 MOB-23 token emit related code 2021-11-07 14:14:22 +02:00
Henrik Lepson
eca3f92468 MOB-41 small changes to the test app 2021-11-07 13:49:19 +02:00
TanelOrumaa
44469b8533 Merge remote-tracking branch 'origin/authJWT' into MOB-42 2021-11-07 13:30:09 +02:00
Henrik Lepson
cc3a3c10d6 MOB-42 app can be launched with deep links 2021-11-05 20:21:22 +02:00
Henrik Lepson
08430c897c MOB-37 added english translation to the app 2021-10-30 17:07:17 +03:00
Henrik Lepson
ebc541af08 MOB-13 pin2 fragment created 2021-10-30 16:51:38 +03:00
Henrik Lepson
a9336b790f MOB-41 created a base for app that can launch authapp 2021-10-26 11:42:58 +03:00
Henrik Lepson
364fc7c45b MOB-40 started to create UI for auth when launched with intent 2021-10-21 21:22:11 +03:00
Henrik Lepson
71db5cc9e6 Merge pull request #9 from TanelOrumaa/iter3UI
Iter3 UI
2021-10-21 20:31:26 +03:00
Lemmo Lavonen
62888a7299 Add a method for signing the auth token hash. 2021-10-19 00:58:53 +03:00
Henrik Lepson
3e5f02f842 made some changes related to the demo next week 2021-10-16 14:02:15 +03:00
Henrik Lepson
bd686739fc MOB-40 changed nav logic, refactored Home, Pin and Can fragments 2021-10-16 13:20:55 +03:00
Henrik Lepson
2678fd3c42 MOB-38 MOB-40 PIN1 save functionality added, pin/can nav order changed 2021-10-16 11:02:48 +03:00
Henrik Lepson
1dd11b21fc MOB-38 added PIN1 UI components to settings menu 2021-10-16 09:47:57 +03:00
Henrik Lepson
8637a4182a MOB-39 can can be now stored on the phone 2021-10-15 22:12:06 +03:00
Henrik Lepson
141dfb18db MOB-38 MOB-39 MOB-40 created UI and changed navigation for CAN saving 2021-10-15 18:23:21 +03:00
Henrik Lepson
48817f9057 MOB-38 created basic menu layout, cur no actions 2021-10-15 17:06:00 +03:00
Lemmo Lavonen
850ab8fc66 Use hex to represent bytes. 2021-10-14 19:50:53 +03:00
Lemmo Lavonen
824d33d635 Merge branch 'main' into authJWT 2021-10-14 04:01:47 +03:00
Lemmo Lavonen
ef7015abb8 Refactor (generalise selecting and reading a file, PIN verification and certificate retrieval). 2021-10-14 03:58:49 +03:00
Lemmo Lavonen
29c7ecfa12 Refactor the method for authentication certificate retrieval. 2021-10-13 02:27:19 +03:00
Lemmo Lavonen
9c48cc9c1a Fix authentication certificate retrieval. 2021-10-12 12:18:06 +03:00
Lemmo Lavonen
25c01803cb auth cert bug fix progress 2021-10-12 03:04:00 +03:00
Lemmo Lavonen
1c8a606376 Add a method for getting the authentication certificate (WIP). 2021-10-12 00:36:08 +03:00
Lemmo Lavonen
d2ad8920a1 Add a method for verifying PIN1. 2021-10-12 00:34:06 +03:00
TanelOrumaa
96595d924b Added last updated times for wiki pages. 2021-10-10 23:52:59 +03:00
TanelOrumaa
4ae5d3c0c4 Changed formatting for Readme 2021-10-10 23:40:36 +03:00
TanelOrumaa
490a04bb44 Update Readme with information on how to run the app 2021-10-10 23:39:37 +03:00
106 changed files with 4315 additions and 237 deletions

View File

@@ -59,4 +59,17 @@ dependencies {
//For cryptography //For cryptography
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69' implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69'
//SecureDataStoring
implementation("androidx.security:security-crypto:1.0.0")
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
'org.bouncycastle:bcprov-jdk15on:1.60',
'io.jsonwebtoken:jjwt-gson:0.11.2'
implementation 'com.koushikdutta.ion:ion:3.1.0'
// Retrofit + Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
} }

View File

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

View File

@@ -2,6 +2,7 @@ package com.tarkvaraprojekt.mobileauthapp
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.tech.IsoDep import android.nfc.tech.IsoDep
import android.os.Bundle import android.os.Bundle
@@ -10,14 +11,18 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import java.lang.Exception import java.lang.Exception
import kotlin.concurrent.thread import kotlin.system.exitProcess
/** /**
* Fragment that asks the user to detect the ID card with mobile NFC chip. * Fragment that asks the user to detect the ID card with mobile NFC chip.
@@ -28,8 +33,12 @@ class AuthFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels() private val viewModel: SmartCardViewModel by activityViewModels()
private val intentParameters: ParametersViewModel by activityViewModels()
private var binding: FragmentAuthBinding? = null private var binding: FragmentAuthBinding? = null
private val args: CanFragmentArgs by navArgs()
private lateinit var timer: CountDownTimer private lateinit var timer: CountDownTimer
private var timeRemaining: Int = 90 private var timeRemaining: Int = 90
@@ -49,9 +58,9 @@ class AuthFragment : Fragment() {
override fun onTick(p0: Long) { override fun onTick(p0: Long) {
timeRemaining-- timeRemaining--
if (timeRemaining == 0) { if (timeRemaining == 0) {
binding!!.timeCounter.text = getString(R.string.no_time) binding?.timeCounter?.text = getString(R.string.no_time)
} else { } else {
binding!!.timeCounter.text = getString(R.string.time_left, timeRemaining) binding?.timeCounter?.text = getString(R.string.time_left, timeRemaining)
} }
} }
@@ -78,10 +87,22 @@ class AuthFragment : Fragment() {
card.use { card.use {
try { try {
val comms = Comms(it, viewModel.userCan) val comms = Comms(it, viewModel.userCan)
val response = comms.readPersonalData(byteArrayOf(1, 2, 6)) if (args.auth) {
val jws = Authenticator(comms).authenticate(
intentParameters.challenge,
intentParameters.authUrl,
viewModel.userPin
)
intentParameters.setToken(jws)
} else {
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
viewModel.setUserFirstName(response[1]) viewModel.setUserFirstName(response[1])
viewModel.setUserLastName(response[0]) viewModel.setUserLastName(response[0])
viewModel.setUserIdentificationNumber(response[2]) viewModel.setUserIdentificationNumber(response[2])
viewModel.setGender(response[3])
viewModel.setCitizenship(response[4])
viewModel.setExpiration(response[5])
}
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.data_read) binding!!.timeCounter.text = getString(R.string.data_read)
} }
@@ -89,6 +110,8 @@ class AuthFragment : Fragment() {
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.no_success) binding!!.timeCounter.text = getString(R.string.no_success)
} }
// 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 // Gives user some time to read the error message
Thread.sleep(1000) Thread.sleep(1000)
goToTheStart() goToTheStart()
@@ -101,13 +124,30 @@ class AuthFragment : Fragment() {
private fun goToNextFragment() { private fun goToNextFragment() {
timer.cancel() timer.cancel()
if (args.auth) {
val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile)
findNavController().navigate(action)
} else {
findNavController().navigate(R.id.action_authFragment_to_userFragment) findNavController().navigate(R.id.action_authFragment_to_userFragment)
} }
}
private fun goToTheStart() { private fun goToTheStart() {
viewModel.clearUserInfo() viewModel.clearUserInfo()
timer.cancel() timer.cancel()
if (args.reading) {
findNavController().navigate(R.id.action_authFragment_to_homeFragment) findNavController().navigate(R.id.action_authFragment_to_homeFragment)
} else {
if (!args.mobile) {
//Currently for some reason the activity is not killed entirely. Must be looked into further.
requireActivity().finish()
exitProcess(0)
} else {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -1,18 +1,24 @@
package com.tarkvaraprojekt.mobileauthapp package com.tarkvaraprojekt.mobileauthapp
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
/** /**
* Fragment that deals with asking the user for six digit CAN * Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved
* then the fragment is skipped immediately and if the CAN is not saved then the user
* is asked whether it should be saved for the future or not before continuing.
*/ */
class CanFragment : Fragment() { class CanFragment : Fragment() {
@@ -20,6 +26,12 @@ class CanFragment : Fragment() {
private var binding: FragmentCanBinding? = null private var binding: FragmentCanBinding? = null
// 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -31,28 +43,98 @@ class CanFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
checkIfSkip()
binding!!.nextButton.setOnClickListener { goToNextFragment() } // 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!!.nextButton.setOnClickListener { checkEnteredCan() }
binding!!.cancelButton.setOnClickListener { goToTheStart() } binding!!.cancelButton.setOnClickListener { goToTheStart() }
} }
private fun goToNextFragment() { /**
val enteredCan = binding!!.canEditText.editText?.text.toString() * Checks if the current fragment can be skipped or not.
if (enteredCan.length != 6) { * If the user has CAN saved on the device there is no need to ask it again.
Toast.makeText(requireContext(), getString(R.string.length_can), Toast.LENGTH_SHORT) */
.show() private fun checkIfSkip() {
} else { if (viewModel.userCan.length == 6) {
viewModel.setUserCan( goToTheNextFragment()
binding!!.canEditText.editText?.text.toString()
)
findNavController().navigate(R.id.action_canFragment_to_authFragment)
} }
} }
/**
* 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)
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() { private fun goToTheStart() {
viewModel.clearUserInfo() // TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment.
if (args.saving) {
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
} else if (args.auth) {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
findNavController().navigate(R.id.action_canFragment_to_homeFragment) findNavController().navigate(R.id.action_canFragment_to_homeFragment)
} }
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()

View File

@@ -1,19 +1,33 @@
package com.tarkvaraprojekt.mobileauthapp package com.tarkvaraprojekt.mobileauthapp
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import java.lang.Exception
/**
* HomeFragment is only shown to the user when then the user launches the application. When the application
* is launched by another application or a website then this Fragment will be skipped.
* This fragment uses the fields from the MainActivity by casting the activity to MainActivity.
* This might not be the best practice, but the application uses a single activity design so it should
* always work.
*/
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels() private val viewModel: SmartCardViewModel by activityViewModels()
private val intentParams: ParametersViewModel by activityViewModels()
private var binding: FragmentHomeBinding? = null private var binding: FragmentHomeBinding? = null
override fun onCreateView( override fun onCreateView(
@@ -22,17 +36,104 @@ class HomeFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initialChecks()
binding!!.beginButton.setOnClickListener { goToNextFragment() } 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){
try {
if (mobile) {
// We use !! because we want an exception when something is not right.
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
} else { //Website
// Currently the test website won't send the authUrl parameter
//Log.i("intentDebugging", requireActivity().intent.data.toString())
intentParams.setChallenge(requireActivity().intent.data!!.getQueryParameter("challenge")!!)
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
}
} catch (e: Exception) {
// There was a problem with parameters, which means that authentication is not possible.
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
goToTheNextFragment(true, mobile)
}
binding!!.beginButton.setOnClickListener { goToTheNextFragment() }
} }
private fun goToNextFragment() { /**
findNavController().navigate(R.id.action_homeFragment_to_pinFragment) * Method where all the initial checks that should be done before any user input is accepted should be added.
*/
private fun initialChecks() {
viewModel.checkCan(requireContext())
viewModel.checkPin(requireContext())
displayStates()
}
/**
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
*/
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)
} else {
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(reading = true, auth = false, mobile = mobile)
findNavController().navigate(action)
}
}
/**
* Displays texts that inform the user whether the CAN and PIN 1 are saved on the device or not.
* This might help the user to save some time as checking menu is not necessary unless the user
* wishes to make changes to the saved CAN or PIN 1.
*/
private fun displayStates() {
canState()
pinState()
}
/**
* Checks the state of the CAN, saved or not saved. Updates the text and logo.
*/
private fun canState() {
if (viewModel.userCan.length == 6) {
binding!!.canStatusText.text = getString(R.string.can_status_saved)
binding!!.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
} else {
binding!!.canStatusText.text = getString(R.string.can_status_negative)
binding!!.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
}
}
/**
* 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)
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@@ -1,24 +1,51 @@
package com.tarkvaraprojekt.mobileauthapp package com.tarkvaraprojekt.mobileauthapp
import android.nfc.NfcAdapter
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
/**
* The only activity of the application (single activity design).
*/
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var navigationController: NavController private lateinit var navigationController: NavController
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
var menuAvailable: Boolean = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater) val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navigationController = navHostFragment.navController navigationController = navHostFragment.navController
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.menu_settings_option -> {
if (menuAvailable) {
navigationController.navigate(R.id.action_homeFragment_to_settingsFragment)
true
} else {
Toast.makeText(this, getString(R.string.unavailable), Toast.LENGTH_SHORT).show()
false
}
}
else -> super.onOptionsItemSelected(item)
}
} }

View File

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

View File

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

View File

@@ -1,18 +1,24 @@
package com.tarkvaraprojekt.mobileauthapp package com.tarkvaraprojekt.mobileauthapp
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
/** /**
* Fragment that deals with asking the user for PIN1 * Fragment that deals with asking the user for PIN 1. If the user has already saved the PIN 1 then it is not asked again
* and the fragment is skipped and if the PIN 1 is not saved then the user is asked whether it should be saved or
* not before continuing.
*/ */
class PinFragment : Fragment() { class PinFragment : Fragment() {
@@ -20,6 +26,12 @@ class PinFragment : Fragment() {
private var binding: FragmentPinBinding? = null private var binding: FragmentPinBinding? = null
// 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()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -31,33 +43,98 @@ class PinFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
checkIfSkip()
binding!!.nextButton.setOnClickListener { goToNextFragment() } // If the user arrives from the settings menu then the button says
// save instead of continue.
if (args.saving) {
binding!!.nextButton.text = getString(R.string.save_text)
}
binding!!.nextButton.setOnClickListener { checkEnteredPin() }
binding!!.cancelButton.setOnClickListener { goToTheStart() } binding!!.cancelButton.setOnClickListener { goToTheStart() }
// Currently PIN 1 is not required and thus this step is immediately skipped.
// In the future the UI flow will be changed in the nav_graph.
goToNextFragment()
} }
private fun goToNextFragment() { /**
val enteredPin1 = binding!!.pinEditText.editText?.text.toString() * Checks if the current fragment can be skipped or not.
if (enteredPin1.length in 4..12) { * If the user has PIN 1 saved on the device or PIN 1 is not required
viewModel.setUserPin( * then the PIN 1 won't be asked.
binding!!.pinEditText.editText?.text.toString() */
) private fun checkIfSkip() {
findNavController().navigate(R.id.action_pinFragment_to_canFragment) if (args.reading) {
goToTheNextFragment()
} else if (viewModel.userPin.length in 4..12) {
goToTheNextFragment()
}
}
/**
* 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)
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 { } else {
// Currently it is not important to enter PIN1 so we will allow the user to leave this field empty val storePinQuestion = getDialog()
//Toast.makeText(requireContext(), getString(R.string.length_pin), Toast.LENGTH_SHORT) storePinQuestion?.show()
// .show() }
viewModel.setUserPin("1234") } else {
findNavController().navigate(R.id.action_pinFragment_to_canFragment) 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() { private fun goToTheStart() {
viewModel.clearUserInfo() if (args.saving) {
findNavController().navigate(R.id.action_pinFragment_to_homeFragment) findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
} else if (args.auth) {
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -0,0 +1,110 @@
package com.tarkvaraprojekt.mobileauthapp
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs
import com.google.gson.JsonObject
import com.koushikdutta.ion.Ion
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import com.tarkvaraprojekt.mobileauthapp.network.BASE_URL
import com.tarkvaraprojekt.mobileauthapp.network.TokenApi
import com.tarkvaraprojekt.mobileauthapp.network.TokenApiService
import com.tarkvaraprojekt.mobileauthapp.network.TokenItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.system.exitProcess
/**
* ResultFragment is used to create a JWT and to send response to the website/application
* that launched the MobileAuthApp. If the mobile auth app was started by a website
* the result is sent to a server with a POST request.
*/
class ResultFragment : Fragment() {
private val paramsModel: ParametersViewModel by activityViewModels()
private var binding: FragmentResultBinding? = null
private val args: ResultFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentResultBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.resultBackButton.setOnClickListener {
// if (args.mobile) {
// createResponse()
// }
postToken()
}
}
/**
* Makes a POST request to the backend server with a tokenItem
*/
fun postToken() {
val json = JsonObject()
json.addProperty("token", paramsModel.token)
json.addProperty("challenge", paramsModel.challenge)
Ion.getDefault(activity).getConscryptMiddleware().enable(false)
Ion.with(activity)
.load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication")
.setJsonObjectBody(json)
.asJsonObject()
.setCallback { e, result ->
// do stuff with the result or error
Log.i("Log thingy", result.toString())
}
// CoroutineScope(Dispatchers.Default).launch {
// val response = TokenApi.retrofitService.postToken(jsonBody)
// Log.v("Response", response.message())
// if (response.isSuccessful) {
// //Success scenario here
// } else {
// //Failure scenario here
// if (args.mobile) {
// createResponse(false)
// } else {
// //Currently for some reason the activity is not killed entirely. Must be looked into further.
// requireActivity().finish()
// exitProcess(0)
// }
// }
// }
}
/**
* Only used when the MobileAuthApp was launched by an app. Not for website use.
*/
private fun createResponse(success: Boolean = true) {
val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
val resultIntent = Intent()
requireActivity().setResult(responseCode, resultIntent)
requireActivity().finish()
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View File

@@ -33,13 +33,25 @@ class UserFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
displayInformation()
binding!!.userName.text =
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
binding!!.identificationNumber.text = viewModel.userIdentificationNumber
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 =
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
binding!!.identificationNumber.text = viewModel.userIdentificationNumber
binding!!.gender.text = viewModel.gender
binding!!.expiration.text = viewModel.expiration.replace(" ", "/")
binding!!.citizenship.text = viewModel.citizenship
}
/**
* Navigates user back to the start and also deletes any temporary information.
*/
private fun goToTheStart() { private fun goToTheStart() {
viewModel.clearUserInfo() viewModel.clearUserInfo()
findNavController().navigate(R.id.action_userFragment_to_homeFragment) findNavController().navigate(R.id.action_userFragment_to_homeFragment)

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
package com.tarkvaraprojekt.mobileauthapp.model
import androidx.lifecycle.ViewModel
class ParametersViewModel: ViewModel() {
private var _challenge: String = ""
val challenge get() = _challenge
private var _authUrl: String = ""
val authUrl get() = _authUrl
private var _token: String = ""
val token get() = _token
fun setChallenge(newChallenge: String) {
_challenge = newChallenge
}
fun setAuthUrl(newAuthUrl: String) {
_authUrl = newAuthUrl
}
fun setToken(newToken: String) {
_token = newToken
}
}

View File

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

View File

@@ -0,0 +1,34 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,10 @@
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -59,7 +62,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/next_text" android:text="@string/next_text"
android:textSize="18sp" android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button" app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" /> app:layout_constraintTop_toBottomOf="@id/card_view" />
@@ -70,7 +73,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/cancel_text" android:text="@string/cancel_text"
android:textSize="18sp" android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/next_button" app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" /> app:layout_constraintTop_toBottomOf="@id/card_view" />

View File

@@ -14,7 +14,10 @@
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -63,7 +66,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/next_text" android:text="@string/next_text"
android:textSize="18sp" android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button" app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" /> app:layout_constraintTop_toBottomOf="@id/card_view" />
@@ -74,7 +77,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/cancel_text" android:text="@string/cancel_text"
android:textSize="18dp" android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/next_button" app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" /> app:layout_constraintTop_toBottomOf="@id/card_view" />

View File

@@ -7,23 +7,87 @@
android:padding="24dp" android:padding="24dp"
tools:context=".HomeFragment"> tools:context=".HomeFragment">
<TextView <LinearLayout
android:id="@+id/home_fragment_text" android:id="@+id/saved_states"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/home_fragment" android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/can_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/can_status_logo"
android:layout_marginStart="12dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/can_status_text"
android:textSize="20sp"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/pin_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/pin_status_logo"
android:layout_marginStart="12dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/pin_status_text"
android:textSize="20sp"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Temporary button for testing purposes -->
<Button <Button
android:id="@+id/begin_button" android:id="@+id/begin_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/begin_text" android:text="@string/begin_text"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/home_fragment_text" android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/saved_states"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>

View File

@@ -14,7 +14,10 @@
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -62,6 +65,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/next_text" android:text="@string/next_text"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button" app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toBottomOf="@id/card_view" /> app:layout_constraintTop_toBottomOf="@id/card_view" />
@@ -72,6 +76,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:text="@string/cancel_text" android:text="@string/cancel_text"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/next_button" app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_view" /> app:layout_constraintTop_toBottomOf="@id/card_view" />

View File

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

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".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"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/result_text"
android:text="@string/result_text"
android:textSize="20sp"
android:padding="12dp"
android:layout_marginVertical="6dp"
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: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

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="12dp"
tools:context=".menu.SettingsFragment">
<com.google.android.material.card.MaterialCardView
android:id="@+id/settings_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout
android:id="@+id/can_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/can_saved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:padding="12dp"
android:text="@string/saved_can" />
<Button
android:id="@+id/can_menu_action"
android:layout_margin="12dp"
android:textSize="15sp"
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: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" />
<Button
android:id="@+id/pin_menu_show"
android:layout_margin="12dp"
android:textSize="15sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/return_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/return_text"
android:layout_margin="24dp"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/settings_card"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -14,7 +14,10 @@
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="1dp"
app:strokeColor="@color/stroke_color"
app:cardElevation="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -54,6 +57,54 @@
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView
android:id="@+id/gender_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/gender_label"
android:textSize="14sp" />
<TextView
android:id="@+id/gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/expiration_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/expiration_label"
android:textSize="14sp" />
<TextView
android:id="@+id/expiration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/citizenship_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/citizenship_label"
android:textSize="14sp" />
<TextView
android:id="@+id/citizenship"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

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

View File

@@ -11,39 +11,85 @@
android:label="fragment_home" android:label="fragment_home"
tools:layout="@layout/fragment_home"> tools:layout="@layout/fragment_home">
<action <action
android:id="@+id/action_homeFragment_to_pinFragment" android:id="@+id/action_homeFragment_to_settingsFragment"
app:destination="@id/pinFragment" app:destination="@id/settingsFragment"
/> app:popUpTo="@id/homeFragment" />
<action
android:id="@+id/action_homeFragment_to_canFragment"
app:destination="@id/canFragment"
app:popUpTo="@id/homeFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/pinFragment" android:id="@+id/pinFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.PinFragment" android:name="com.tarkvaraprojekt.mobileauthapp.PinFragment"
android:label="fragment_pin" android:label="fragment_pin"
tools:layout="@layout/fragment_pin"> tools:layout="@layout/fragment_pin">
<action
android:id="@+id/action_pinFragment_to_canFragment"
app:destination="@id/canFragment"
app:popUpTo="@id/homeFragment" />
<action <action
android:id="@+id/action_pinFragment_to_homeFragment" android:id="@+id/action_pinFragment_to_homeFragment"
app:destination="@id/homeFragment" app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_pinFragment_to_settingsFragment"
app:destination="@id/settingsFragment"
app:popUpTo="@id/settingsFragment"
app:popUpToInclusive="true" />
<argument
android:name="saving"
android:defaultValue="false"
app:argType="boolean" />
<action
android:id="@+id/action_pinFragment_to_authFragment"
app:destination="@id/authFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="reading"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/canFragment" android:id="@+id/canFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.CanFragment" android:name="com.tarkvaraprojekt.mobileauthapp.CanFragment"
android:label="fragment_can" android:label="fragment_can"
tools:layout="@layout/fragment_can"> tools:layout="@layout/fragment_can">
<action
android:id="@+id/action_canFragment_to_authFragment"
app:destination="@id/authFragment"
app:popUpTo="@id/homeFragment"/>
<action <action
android:id="@+id/action_canFragment_to_homeFragment" android:id="@+id/action_canFragment_to_homeFragment"
app:destination="@id/homeFragment" app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_canFragment_to_settingsFragment"
app:destination="@id/settingsFragment"
app:popUpTo="@id/settingsFragment"
app:popUpToInclusive="true" />
<argument
android:name="saving"
android:defaultValue="false"
app:argType="boolean" />
<action
android:id="@+id/action_canFragment_to_pinFragment"
app:destination="@id/pinFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="reading"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/authFragment" android:id="@+id/authFragment"
@@ -59,6 +105,22 @@
app:destination="@id/homeFragment" app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_authFragment_to_resultFragment"
app:destination="@id/resultFragment"
app:popUpTo="@id/homeFragment" />
<argument
android:name="auth"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="reading"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/userFragment" android:id="@+id/userFragment"
@@ -71,4 +133,31 @@
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
</fragment> </fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.menu.SettingsFragment"
android:label="fragment_settings"
tools:layout="@layout/fragment_settings">
<action
android:id="@+id/action_settingsFragment_to_canFragment"
app:destination="@id/canFragment" />
<action
android:id="@+id/action_settingsFragment_to_pinFragment"
app:destination="@id/pinFragment" />
<action
android:id="@+id/action_settingsFragment_to_homeFragment"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/resultFragment"
android:name="com.tarkvaraprojekt.mobileauthapp.ResultFragment"
android:label="fragment_result"
tools:layout="@layout/fragment_result" >
<argument
android:name="mobile"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
</navigation> </navigation>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Must translate to English, but should work now -->
<string name="app_name">NFC authentication</string>
<string name="home_fragment">Work in progress</string>
<!-- string resources for HomeFragment -->
<string name="pin_status_saved">PIN 1 saved</string>
<string name="pin_status_negative">PIN 1 not saved</string>
<string name="can_status_saved">CAN saved</string>
<string name="can_status_negative">CAN not saved</string>
<string name="begin_text">READ ID CARD</string>
<string name="next_text">NEXT</string>
<string name="cancel_text">CANCEL</string>
<string name="save_text">SAVE</string>
<string name="deny_text">NO</string>
<string name="return_text">BACK</string>
<!-- string resources for PinFragment -->
<string name="pin_fragment">Please enter PIN 1</string>
<string name="enter_pin">PIN 1</string>
<string name="example_pin">Example. 1234</string>
<string name="length_pin">Allowed length for PIN 1 is 4..12</string>
<string name="pin_save_request">PIN 1 is currently not saved. Do you wish to save the entered PIN 1? Saved PIN 1 will be entered automatically in the future. Saved PIN 1 can be changed and deleted in the settings menu.</string>
<string name="save_pin_title">Save PIN 1</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Please enter PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Example. 123456</string>
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
<!-- string resources for CanFragment -->
<string name="example_can">Example. 123456</string>
<string name="text_can">CAN</string>
<string name="enter_can">Enter ID card\'s CAN (Card Access Number)</string>
<string name="length_can">Length of the CAN is wrong</string>
<string name="card_detected">Card detected. Hold it against the phone.</string>
<string name="data_read">Data read. You can continue.</string>
<string name="save_can_title">Save CAN</string>
<!-- string resources for AuthFragment layout -->
<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 resources for UserFragment layout -->
<string name="user_name_label">NAME</string>
<string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">IDENTIFICATION NUMBER</string>
<string name="expiration_label">DATE OF EXPIRY</string>
<string name="citizenship_label">CITIZENSHIP</string>
<string name="gender_label">SEX</string>
<string name="clear_button">FORGET</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
<!-- menu -->
<string name="menu_settings_title">Settings</string>
<string name="menu_language_title">Language</string>
<string name="menu_action_unavailable">Currently unavailable</string>
<string name="saved_can">CAN: %s</string>
<string name="can_add">Add CAN</string>
<string name="can_delete">Delete CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Add PIN1</string>
<string name="pin1_delete">Delete PIN1</string>
<string name="missing">not saved</string>
<string name="show">SHOW</string>
<string name="hide">HIDE</string>
<string name="hidden_pin">****</string>
<string name="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>
</resources>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NFC authentication</string>
<string name="home_fragment">Work in progress</string>
<string name="begin_text">LOE ID KAARTI</string>
<string name="next_text">EDASI</string>
<string name="cancel_text">KATKESTA</string>
<string name="save_text">SALVESTA</string>
<string name="deny_text">EI</string>
<string name="return_text">TAGASI</string>
<!-- string resources for HomeFragment -->
<string name="pin_status_saved">PIN 1 on salvestatud</string>
<string name="pin_status_negative">PIN 1 ei ole salvestatud</string>
<string name="can_status_saved">CAN on salvestatud</string>
<string name="can_status_negative">CAN ei ole salvestatud</string>
<!-- string resources for PinFragment -->
<string name="pin_fragment">Palun sisesta PIN 1</string>
<string name="enter_pin">PIN 1</string>
<string name="example_pin">Näide. 1234</string>
<string name="length_pin">PIN 1 lubatud pikkus on 4..12</string>
<string name="pin_save_request">Praegu ei ole rakenduses PIN 1 salvestatud. Kas sa soovid sisestatud PIN 1-te salvestada? Sellisel juhul sisestatakse see järgmisel korral automaatselt. Salvestatud PIN 1-te saab alati menüüs muuta ja kustutada.</string>
<string name="save_pin_title">Salvesta PIN 1</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Palun sisesta PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Näide. 123456</string>
<string name="length_pin2">PIN 2 lubatud pikkus on 5..12</string>
<!-- string resources for CanFragment -->
<string name="example_can">Näide. 123456</string>
<string name="text_can">CAN</string>
<string name="enter_can">Sisesta ID kaardi CAN (Card Access Number)</string>
<string name="length_can">CANi pikkus on vale</string>
<string name="card_detected">Kaart on tuvastatud. Hoia kaarti vastu telefoni.</string>
<string name="data_read">Andmed loetud. Võid edasi minna.</string>
<string name="can_save_request">Praegu ei ole rakenduses CAN salvestatud. Kas sa soovid sisestatud CANi salvestada? Sellisel juhul sisestatakse see järgmisel korral automaatselt. Salvestatud CANi saab alati menüüs muuta ja kustutada.</string> <string name="save_can_title">Salvesta CAN</string>
<!-- string resources for AuthFragment layout -->
<string name="auth_instruction_text">ID kaardiga ühenduse loomiseks pane kaart vastu telefoni</string>
<string name="time_left">Aega on jäänud %d sek</string>
<string name="no_time">Aeg on otsas</string>
<string name="no_success">Vale CAN</string>
<!-- string resources for UserFragment layout -->
<string name="user_name_label">NIMI</string>
<string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">ISIKUKOOD</string>
<string name="clear_button">UNUSTA</string>
<string name="expiration_label">KEHTIV KUNI</string>
<string name="citizenship_label">KODAKONDSUS</string>
<string name="gender_label">SUGU</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
<!-- menu -->
<string name="menu_settings_title">Seaded</string>
<string name="menu_language_title">Keel</string>
<string name="menu_action_unavailable">Toiming pole hetkel saadaval</string>
<string name="saved_can">CAN: %s</string>
<string name="can_add">Lisa CAN</string>
<string name="can_delete">Kustuta CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Lisa PIN1</string>
<string name="pin1_delete">Kustuta PIN1</string>
<string name="missing">puudub</string>
<string name="show">NÄITA</string>
<string name="hide">PEIDA</string>
<string name="hidden_pin">****</string>
<string name="unavailable">Seaded pole hetkel saadaval</string>
</resources>

View File

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

View File

@@ -13,5 +13,4 @@
<color name="blue_700">#1976d2</color> <color name="blue_700">#1976d2</color>
<color name="orange_200">#ffcc80</color> <color name="orange_200">#ffcc80</color>
<color name="orange_700">#f57c00</color> <color name="orange_700">#f57c00</color>
</resources> </resources>

View File

@@ -1,34 +1,76 @@
<resources> <resources>
<string name="app_name">Mobile Authenticator</string> <string name="app_name">NFC authentication</string>
<string name="home_fragment">Work in progress</string> <string name="home_fragment">Work in progress</string>
<string name="begin_text">ALUSTA</string> <string name="begin_text">READ ID CARD</string>
<string name="next_text">EDASI</string> <string name="next_text">NEXT</string>
<string name="cancel_text">KATKESTA</string> <string name="cancel_text">CANCEL</string>
<string name="save_text">SAVE</string>
<string name="deny_text">NO</string>
<string name="return_text">BACK</string>
<!-- string resources for HomeFragment -->
<string name="pin_status_saved">PIN 1 saved</string>
<string name="pin_status_negative">PIN 1 not saved</string>
<string name="can_status_saved">CAN saved</string>
<string name="can_status_negative">CAN not saved</string>
<!-- string resources for PinFragment --> <!-- string resources for PinFragment -->
<string name="pin_fragment">Palun sisesta PIN 1</string> <string name="pin_fragment">Please enter PIN 1</string>
<string name="enter_pin">PIN 1</string> <string name="enter_pin">PIN 1</string>
<string name="example_pin">Näide. 1234</string> <string name="example_pin">Example. 1234</string>
<string name="length_pin">PIN 1 lubatud pikkus on 4..12</string> <string name="length_pin">Allowed length for PIN 1 is 4..12</string>
<string name="pin_save_request">PIN 1 is currently not saved. Do you wish to save the entered PIN 1? Saved PIN 1 will be entered automatically in the future. Saved PIN 1 can be changed and deleted in the settings menu.</string>
<string name="save_pin_title">Save PIN 1</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Please enter PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Example. 123456</string>
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
<!-- string resources for CanFragment --> <!-- string resources for CanFragment -->
<string name="example_can">Näide. 123456</string> <string name="example_can">Example. 123456</string>
<string name="text_can">CAN</string> <string name="text_can">CAN</string>
<string name="enter_can">Sisesta ID kaardi CAN (Card Access Number)</string> <string name="enter_can">Enter ID card\'s CAN (Card Access Number)</string>
<string name="length_can">CANi pikkus on vale</string> <string name="length_can">Length of the CAN is wrong</string>
<string name="card_detected">Kaart on tuvastatud. Hoia kaarti vastu telefoni.</string> <string name="card_detected">Card detected. Hold it against the phone.</string>
<string name="data_read">Andmed loetud. Võid edasi minna.</string> <string name="data_read">Data read. You can continue.</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="save_can_title">Save CAN</string>
<!-- string resources for AuthFragment layout --> <!-- string resources for AuthFragment layout -->
<string name="auth_instruction_text">ID kaardiga ühenduse loomiseks pane kaart vastu telefoni</string> <string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
<string name="time_left">Aega on jäänud %d sek</string> <string name="time_left">Time left %d sek</string>
<string name="no_time">Aeg on otsas</string> <string name="no_time">No time left</string>
<string name="no_success">Vale CAN</string> <string name="no_success">Wrong CAN</string>
<!-- string resources for UserFragment layout --> <!-- string resources for UserFragment layout -->
<string name="user_name_label">NIMI</string> <string name="user_name_label">NAME</string>
<string name="user_name">%1$s %2$s</string> <string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">ISIKUKOOD</string> <string name="identification_number_label">IDENTIFICATION NUMBER</string>
<string name="clear_button">UNUSTA</string> <string name="expiration_label">DATE OF EXPIRY</string>
<string name="citizenship_label">CITIZENSHIP</string>
<string name="gender_label">SEX</string>
<string name="clear_button">FORGET</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
<!-- menu -->
<string name="menu_settings_title">Settings</string>
<string name="menu_language_title">Language</string>
<string name="menu_action_unavailable">Currently unavailable</string>
<string name="saved_can">CAN: %s</string>
<string name="can_add">Add CAN</string>
<string name="can_delete">Delete CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Add PIN 1</string>
<string name="pin1_delete">Delete PIN 1</string>
<string name="missing">not saved</string>
<string name="show">SHOW</string>
<string name="hide">HIDE</string>
<string name="hidden_pin">****</string>
<string name="unavailable">Settings currently unavailable</string>
</resources> </resources>

View File

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

View File

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

View File

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

16
TestMobileApp/.gitignore vendored Normal file
View File

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

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

@@ -0,0 +1 @@
/build

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
package com.example.testmobileapp
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.example.testmobileapp.databinding.ActivityMainBinding
import com.koushikdutta.ion.Ion
/**
* Test mobile app to demonstrate how other applications can use MobileAuthApp.
* Single purpose app that launches the MobileAuthApp and gets the response back (JWT).
*/
class MainActivity : AppCompatActivity() {
private lateinit var authLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
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)
}
if (response.resultCode == Activity.RESULT_CANCELED) {
binding.loginTextView.text = getString(R.string.auth_failure)
}
}
binding.loginOptionNfcButton.setOnClickListener { launchAuth() }
//binding.loginOptionNfcButton.setOnClickListener { getData() }
}
/**
* Method that creates an intent to launch the MobileAuthApp
*/
private fun launchAuth(challenge: String = "challenge", authUrl: String = "authUrl") {
val launchIntent = Intent()
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
launchIntent.putExtra("action", "auth")
launchIntent.putExtra("challenge", challenge)
launchIntent.putExtra("authUrl", authUrl)
launchIntent.putExtra("mobile", true)
authLauncher.launch(launchIntent)
}
/**
* Method for retrieving data from an endpoint.
* Ion library is used as it is very convenient for making simple GET requests.
*/
private fun getData() {
// Enter the server endpoint address to here
val baseUrl = "enter-base-url-here"
val url = "$baseUrl/auth/challenge"
Ion.with(applicationContext)
.load(url)
.asJsonObject()
.setCallback { _, result ->
try {
// Get data from the result and call launchAuth method
val challenge = result.asJsonObject["nonce"].toString()
launchAuth(challenge, baseUrl)
} catch (e: Exception) {
Log.i("GETrequest", "was unsuccessful")
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TestMobileApp</string>
<string name="login_text">Login</string>
<string name="choose_login_method">Choose login method</string>
<string name="method_nfc">NFC auth</string>
<string name="auth_success">Successful response</string>
<string name="auth_failure">Response failed</string>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TestMobileApp</string>
<string name="login_text">Logi sisse</string>
<string name="choose_login_method">Vali sobiv meetod</string>
<string name="method_nfc">NFC auth</string>
<string name="auth_success">Vastus kätte saadud</string>
<string name="auth_failure">Vastust ei õnnestunud kätte saada</string>
</resources>

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
<resources>
<string name="app_name">TestMobileApp</string>
<string name="login_text">Login</string>
<string name="choose_login_method">Choose login method</string>
<string name="method_nfc">NFC auth</string>
<string name="auth_success">Successful response</string>
<string name="auth_failure">Response failed</string>
</resources>

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

185
TestMobileApp/gradlew vendored Normal file
View File

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

89
TestMobileApp/gradlew.bat vendored Normal file
View File

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

View File

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

33
demoBackend/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2007-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if (mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if (mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if (!outputFile.getParentFile().exists()) {
if (!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}

Binary file not shown.

View File

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

39
demoBackend/README.md Normal file
View File

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

310
demoBackend/mvnw vendored Normal file
View File

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

182
demoBackend/mvnw.cmd vendored Normal file
View File

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

116
demoBackend/pom.xml Normal file
View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tarkvaratehnika</groupId>
<artifactId>demoBackend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demoBackend</name>
<description>demoBackend</description>
<properties>
<java.version>11</java.version>
<kotlin.version>1.5.31</kotlin.version>
<caffeine.version>2.8.5</caffeine.version>
<javaxcache.version>1.1.1</javaxcache.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.webeid.security</groupId>
<artifactId>authtoken-validation</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>${javaxcache.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>jcache</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>gitlab</id>
<url>https://gitlab.com/api/v4/projects/19948337/packages/maven</url>
</repository>
</repositories>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package com.tarkvaratehnika.demobackend
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
import org.springframework.boot.runApplication
@SpringBootApplication(exclude=[SecurityAutoConfiguration::class])
class DemoBackendApplication
fun main(args: Array<String>) {
runApplication<DemoBackendApplication>(*args)
}

View File

@@ -0,0 +1,16 @@
package com.tarkvaratehnika.demobackend.config
class ApplicationConfiguration {
companion object {
// URL for intent, do not edit.
val AUTH_APP_LAUNCH_INTENT = "authapp://start/"
// Endpoint for challenge.
val CHALLENGE_ENDPOINT_URL = "/auth/challenge"
// Endpoint for authentication
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
val WEBSITE_ORIGIN_URL = "https://6bb0-85-253-195-252.ngrok.io"
}
}

View File

@@ -0,0 +1,152 @@
package com.tarkvaratehnika.demobackend.config
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.webeid.security.exceptions.JceException
import org.webeid.security.nonce.NonceGenerator
import org.webeid.security.nonce.NonceGeneratorBuilder
import org.webeid.security.validator.AuthTokenValidator
import org.webeid.security.validator.AuthTokenValidatorBuilder
import java.io.IOException
import java.net.URI
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit
import javax.cache.Cache
import javax.cache.CacheManager
import javax.cache.Caching
import javax.cache.configuration.CompleteConfiguration
import javax.cache.configuration.FactoryBuilder
import javax.cache.configuration.MutableConfiguration
import javax.cache.expiry.CreatedExpiryPolicy
import javax.cache.expiry.Duration
@Configuration
class ValidationConfiguration {
private val NONCE_TTL_MINUTES: Long = 5
private val CACHE_NAME = "nonceCache"
private val CERTS_RESOURCE_PATH = "/certs/"
private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
private val TRUSTSTORE_PASSWORD = "changeit"
@Bean
fun cacheManager(): CacheManager {
return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager
}
@Bean
fun nonceCache(): Cache<String, ZonedDateTime>? {
val cacheManager: CacheManager = cacheManager()
var cache =
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
if (cache == null) {
cache = createNonceCache(cacheManager)
}
return cache
}
@Bean
fun generator(): NonceGenerator? {
return NonceGeneratorBuilder()
.withNonceTtl(java.time.Duration.ofMinutes(NONCE_TTL_MINUTES))
.withNonceCache(nonceCache())
.build()
}
private fun createNonceCache(cacheManager: CacheManager): Cache<String?, ZonedDateTime?>? {
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
.setTypes(String::class.java, ZonedDateTime::class.java)
.setExpiryPolicyFactory(
FactoryBuilder.factoryOf(
CreatedExpiryPolicy(
Duration(
TimeUnit.MINUTES,
NONCE_TTL_MINUTES + 1
)
)
)
)
return cacheManager.createCache(CACHE_NAME, cacheConfig)
}
@Bean
fun loadTrustedCACertificatesFromCerFiles() : Array<X509Certificate> {
val caCertificates = ArrayList<X509Certificate>()
try {
val certFactory = CertificateFactory.getInstance("X.509")
val resolver = PathMatchingResourcePatternResolver()
val resources = resolver.getResources("$CERTS_RESOURCE_PATH/*.cer")
resources.forEach { resource ->
val caCertificate = certFactory.generateCertificate(resource.inputStream) as X509Certificate
caCertificates.add(caCertificate)
}
} catch (e : Exception) {
when (e){
is CertificateException, is IOException -> {
throw RuntimeException("Error initializing trusted CA certificates. $e")
}
}
}
return caCertificates.toTypedArray()
}
@Bean
fun loadTrustedCACertificatesFromTrustStore() : Array<X509Certificate> {
val caCertificates = ArrayList<X509Certificate>()
ValidationConfiguration::class.java.getResourceAsStream("$CERTS_RESOURCE_PATH/$TRUSTED_CERTIFICATES_JKS").use { inputStream ->
try {
if (inputStream == null) {
// No truststore files found.
return arrayOf()
}
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(inputStream, TRUSTSTORE_PASSWORD.toCharArray())
val aliases = keyStore.aliases()
while (aliases.hasMoreElements()) {
val alias = aliases.nextElement()
val certificate = keyStore.getCertificate(alias) as X509Certificate
caCertificates.add(certificate)
}
} catch (e : Exception) {
when (e) {
is IOException, is CertificateException, is KeyStoreException, is NoSuchAlgorithmException -> {
throw RuntimeException("Error initializing trusted CA certificates from trust store. $e")
}
}
}
}
return caCertificates.toTypedArray()
}
@Bean
fun validator() : AuthTokenValidator {
try {
return AuthTokenValidatorBuilder()
.withSiteOrigin(URI.create(ApplicationConfiguration.WEBSITE_ORIGIN_URL))
.withNonceCache(nonceCache())
.withTrustedCertificateAuthorities(*loadTrustedCACertificatesFromCerFiles())
.withTrustedCertificateAuthorities(*loadTrustedCACertificatesFromTrustStore())
.build()
} catch (e : JceException) {
throw RuntimeException("Error building the Web eID auth token validator.", e)
}
}
}

View File

@@ -0,0 +1,3 @@
package com.tarkvaratehnika.demobackend.dto
data class ChallengeDto(val nonce : String)

View File

@@ -0,0 +1,6 @@
package com.tarkvaratehnika.demobackend.security
import com.fasterxml.jackson.annotation.JsonProperty
class AuthTokenDTO (val token : String, val challenge : String) {
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 2020, 2021 The Web eID Project
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.tarkvaratehnika.demobackend.security
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
import org.springframework.security.authentication.AuthenticationServiceException
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.stereotype.Component
import org.webeid.security.exceptions.TokenValidationException
import org.webeid.security.validator.AuthTokenValidator
import java.security.cert.CertificateEncodingException
import java.security.cert.X509Certificate
@Component
class AuthTokenDTOAuthenticationProvider {
companion object {
const val ROLE_USER : String = "ROLE_USER"
}
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
val tokenValidator: AuthTokenValidator = ValidationConfiguration().validator()
@Throws(AuthenticationException::class)
fun authenticate(auth : Authentication) : Authentication {
val authentication = auth as PreAuthenticatedAuthenticationToken
val token = (authentication.credentials as AuthTokenDTO).token
val challenge = (authentication.credentials as AuthTokenDTO).challenge
val authorities = arrayListOf<GrantedAuthority>()
authorities.add(USER_ROLE)
try {
val userCertificate: X509Certificate = tokenValidator.validate(token)
return WebEidAuthentication.fromCertificate(userCertificate, authorities, challenge)
} catch (e : TokenValidationException) {
// Validation failed.
throw AuthenticationServiceException("Token validation failed. " + e.message)
} catch (e : CertificateEncodingException) {
// Failed to extract subject fields from the certificate.
throw AuthenticationServiceException("Incorrect certificate subject fields: " + e.message)
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (c) 2020, 2021 The Web eID Project
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.tarkvaratehnika.demobackend.security
import org.webeid.security.certificate.CertificateData
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import java.security.cert.X509Certificate
import java.util.*
import java.util.concurrent.ThreadLocalRandom
import kotlin.collections.ArrayList
import kotlin.math.log
class WebEidAuthentication(
private val principalName: String,
private val idCode: String,
private val authorities: ArrayList<GrantedAuthority>
) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication {
// Companion object is for static functions.
companion object {
private val loggedInUsers = HashMap<String, Authentication>()
fun fromCertificate(
userCertificate: X509Certificate,
authorities: ArrayList<GrantedAuthority>,
challenge: String
): Authentication {
val principalName = getPrincipalNameFromCertificate(userCertificate)
val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate))
val authentication = WebEidAuthentication(principalName, idCode, authorities)
loggedInUsers[challenge] = authentication
return authentication
}
/**
* Function for getting a Spring authentication object by supplying a challenge.
* TODO: Figure out a more secure solution in the future.
*/
fun fromChallenge(challenge: String): Authentication? {
// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing.
// return null
// }
val auth = loggedInUsers[challenge]
if (auth != null) {
// If challenge is valid, delete the authentication object from the map (so this can only be fetched once).
loggedInUsers.remove(challenge)
} else {
return null
}
return auth
}
// // TODO: DELETE
//
// const val ROLE_USER: String = "ROLE_USER"
// private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
//
// fun addAuth(challenge: String) {
// val authorities = arrayListOf<GrantedAuthority>()
// authorities.add(USER_ROLE)
// val auth = WebEidAuthentication("Somename", "11111111111", authorities)
// loggedInUsers[challenge] = auth
// }
//
//
// // TODO: DELETE UNTIL
private fun getPrincipalNameFromCertificate(userCertificate: X509Certificate): String {
return Objects.requireNonNull(CertificateData.getSubjectGivenName(userCertificate)) + " " +
Objects.requireNonNull(CertificateData.getSubjectSurname(userCertificate))
}
}
}

View File

@@ -0,0 +1,20 @@
package com.tarkvaratehnika.demobackend.web
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class LoginController {
@GetMapping
fun login(model : Model) : String {
model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT)
model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL)
model.addAttribute("originUrl", ApplicationConfiguration.WEBSITE_ORIGIN_URL)
model.addAttribute("loggedInUrl", "/signature")
model.addAttribute("authenticationRequestUrl", ApplicationConfiguration.AUTHENTICATION_ENDPOINT_URL)
return "index"
}
}

View File

@@ -0,0 +1,20 @@
package com.tarkvaratehnika.demobackend.web
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class SignatureController {
@PreAuthorize("hasAuthority('$ROLE_USER')")
@GetMapping("signature")
fun signature(model : Model) : String {
// model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT)
// model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL)
return "signature"
}
}

View File

@@ -0,0 +1,40 @@
package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.security.AuthTokenDTO
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.Authentication
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
@RestController
@RequestMapping("auth")
class AuthenticationController {
private val LOG = LoggerFactory.getLogger(AuthenticationController::class.java)
@PostMapping("authentication", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
fun authenticate(@RequestBody body : String): Authentication {
val parts = body.split("\"")
val authToken = AuthTokenDTO(parts[3], parts[7])
// Create Spring Security Authentication object with supplied token as credentials.
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
// Return authentication object if success.
return AuthTokenDTOAuthenticationProvider().authenticate(auth)
}
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
fun getAuthenticated(@RequestParam challenge: String) : Authentication? {
val auth = WebEidAuthentication.fromChallenge(challenge)
if (auth == null) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Not allowed.")
}
return auth
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2020, 2021 The Web eID Project
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.webeid.security.nonce.NonceGenerator
@RestController
@RequestMapping("auth")
class ChallengeController (val nonceGenerator: NonceGenerator) {
@GetMapping("challenge")
fun challenge(): ChallengeDto {
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
return challengeDto
}
}

View File

@@ -0,0 +1 @@

Binary file not shown.

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