Compare commits
122 Commits
iteration1
...
main
Author | SHA1 | Date |
---|---|---|
TanelOrumaa | 869f11f8a8 | |
TanelOrumaa | bfa5a91ef3 | |
TanelOrumaa | d67c815aad | |
TanelOrumaa | c28fc2be48 | |
TanelOrumaa | c232a1f734 | |
Henrik Lepson | b95115af4f | |
Henrik Lepson | 866c3c24a7 | |
Henrik Lepson | 32336ffb2b | |
Kevin | 7edd8189a4 | |
Henrik Lepson | b889b9cda7 | |
TanelOrumaa | e5931692b6 | |
TanelOrumaa | b66c2386f0 | |
TanelOrumaa | 04933f2705 | |
TanelOrumaa | 8b78ddf51a | |
TanelOrumaa | 13a0a9430f | |
TanelOrumaa | b565f6846d | |
TanelOrumaa | d92656d982 | |
TanelOrumaa | 0da3e17b28 | |
TanelOrumaa | 2b660eeda0 | |
TanelOrumaa | 5719712bef | |
Henrik Lepson | 1e26f83db2 | |
TanelOrumaa | 44430bfab2 | |
TanelOrumaa | 7482c88a4e | |
Henrik Lepson | 74d97827f8 | |
TanelOrumaa | 4096201bef | |
TanelOrumaa | da2dbeb0fc | |
Henrik Lepson | 60207319b7 | |
TanelOrumaa | 7daea4b6c2 | |
Henrik Lepson | e5300dfa5e | |
Henrik Lepson | d4c2a11521 | |
Henrik Lepson | 09c4fa6be3 | |
Henrik Lepson | 63bc89b0e4 | |
Henrik Lepson | 152fd16162 | |
Henrik Lepson | 716b983389 | |
Henrik Lepson | 94fad95364 | |
Henrik Lepson | c33fba1a14 | |
Henrik Lepson | 825335ea5f | |
Henrik Lepson | 0f6f31c995 | |
Henrik Lepson | 762a8c8cc2 | |
Henrik Lepson | 1138abcb11 | |
Henrik Lepson | f085076631 | |
Henrik Lepson | edc444c027 | |
Henrik Lepson | df5febabb7 | |
Henrik Lepson | 1b9a59d4eb | |
TanelOrumaa | bbd5039a0b | |
Henrik Lepson | 2c5430977d | |
Henrik Lepson | 68a7db2e77 | |
Henrik Lepson | a4caf24a35 | |
TanelOrumaa | 5b70a8f997 | |
Henrik Lepson | 168c9be010 | |
TanelOrumaa | 636beeb7f3 | |
TanelOrumaa | 9b0cb1a22d | |
TanelOrumaa | 1d665f02bf | |
TanelOrumaa | 82b1538867 | |
Henrik Lepson | c51f0e4277 | |
Henrik Lepson | aaa8d8f13c | |
Henrik Lepson | d60cecdd26 | |
Henrik Lepson | f0c7ab96bb | |
Henrik Lepson | 1b79eba4a4 | |
TanelOrumaa | c46c3082b7 | |
TanelOrumaa | e4a9a4da1b | |
TanelOrumaa | f9cd30922e | |
Henrik Lepson | 4252e3e637 | |
Henrik Lepson | 64357ca1d3 | |
Henrik Lepson | eca3f92468 | |
TanelOrumaa | 44469b8533 | |
Henrik Lepson | cc3a3c10d6 | |
Henrik Lepson | 08430c897c | |
Henrik Lepson | ebc541af08 | |
Henrik Lepson | a9336b790f | |
Henrik Lepson | 364fc7c45b | |
Henrik Lepson | 71db5cc9e6 | |
Lemmo Lavonen | 62888a7299 | |
Henrik Lepson | 3e5f02f842 | |
Henrik Lepson | bd686739fc | |
Henrik Lepson | 2678fd3c42 | |
Henrik Lepson | 1dd11b21fc | |
Henrik Lepson | 8637a4182a | |
Henrik Lepson | 141dfb18db | |
Henrik Lepson | 48817f9057 | |
Lemmo Lavonen | 850ab8fc66 | |
Lemmo Lavonen | 824d33d635 | |
Lemmo Lavonen | ef7015abb8 | |
Lemmo Lavonen | 29c7ecfa12 | |
Lemmo Lavonen | 9c48cc9c1a | |
Lemmo Lavonen | 25c01803cb | |
Lemmo Lavonen | 1c8a606376 | |
Lemmo Lavonen | d2ad8920a1 | |
TanelOrumaa | 96595d924b | |
TanelOrumaa | 4ae5d3c0c4 | |
TanelOrumaa | 490a04bb44 | |
Henrik Lepson | 86c6a32032 | |
Henrik Lepson | 57ed281aa2 | |
TanelOrumaa | e975a3e262 | |
TanelOrumaa | aeb9a5dada | |
Henrik Lepson | 76fecd766f | |
Henrik Lepson | cc35723c07 | |
Henrik Lepson | d46fb21075 | |
Henrik Lepson | 7803e35ac6 | |
Henrik Lepson | fc56825ed0 | |
Lemmo Lavonen | f93b37c535 | |
Lemmo Lavonen | 0e92b3dd8d | |
Tanel Orumaa | 812647cade | |
TanelOrumaa | f92266f187 | |
Tanel Orumaa | 20d08f8d6c | |
TanelOrumaa | efcb784e9b | |
Henrik Lepson | 634d168378 | |
Lemmo Lavonen | c98eb92c2f | |
Lemmo Lavonen | c30fff7a6f | |
Lemmo Lavonen | 08006dc1ac | |
Henrik Lepson | c4d7da07ad | |
Henrik Lepson | 67ad5296da | |
Henrik Lepson | be14c30b0b | |
Henrik Lepson | a45de7bad4 | |
Henrik Lepson | 276173365a | |
Henrik Lepson | 7582a317bf | |
Henrik Lepson | 804b4472e8 | |
TanelOrumaa | 24b5e89751 | |
Henrik Lepson | 5e92403b93 | |
Henrik Lepson | 8c4b7b613e | |
Henrik Lepson | 724ff0a869 | |
Henrik Lepson | 1da4466e1c |
|
@ -0,0 +1,33 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./MobileAuthApp
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: gradlew permission
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: run gradlew
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: run tests
|
||||
run: ./gradlew test
|
||||
|
||||
- name: upload apk
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: apk
|
||||
path: MobileAuthApp/app/build/outputs/apk/debug/app-debug.apk
|
|
@ -0,0 +1,72 @@
|
|||
.DS_Store
|
||||
.idea/shelf
|
||||
/confluence/target
|
||||
/dependencies/repo
|
||||
/android.tests.dependencies
|
||||
/dependencies/android.tests.dependencies
|
||||
/dist
|
||||
/local
|
||||
/gh-pages
|
||||
/ideaSDK
|
||||
/clionSDK
|
||||
/android-studio/sdk
|
||||
out/
|
||||
/tmp
|
||||
/intellij
|
||||
workspace.xml
|
||||
*.versionsBackup
|
||||
/idea/testData/debugger/tinyApp/classes*
|
||||
/jps-plugin/testData/kannotator
|
||||
/js/js.translator/testData/out/
|
||||
/js/js.translator/testData/out-min/
|
||||
/js/js.translator/testData/out-pir/
|
||||
.gradle/
|
||||
build/
|
||||
!**/src/**/build
|
||||
!**/test/**/build
|
||||
*.iml
|
||||
!**/testData/**/*.iml
|
||||
.idea
|
||||
.idea/libraries/Gradle*.xml
|
||||
.idea/libraries/Maven*.xml
|
||||
.idea/artifacts/PILL_*.xml
|
||||
.idea/artifacts/KotlinPlugin.xml
|
||||
.idea/modules
|
||||
.idea/runConfigurations/JPS_*.xml
|
||||
.idea/runConfigurations/PILL_*.xml
|
||||
.idea/runConfigurations/_FP_*.xml
|
||||
.idea/runConfigurations/_MT_*.xml
|
||||
.idea/libraries
|
||||
.idea/modules.xml
|
||||
.idea/gradle.xml
|
||||
.idea/compiler.xml
|
||||
.idea/inspectionProfiles/profiles_settings.xml
|
||||
.idea/.name
|
||||
.idea/artifacts/dist_auto_*
|
||||
.idea/artifacts/dist.xml
|
||||
.idea/artifacts/ideaPlugin.xml
|
||||
.idea/artifacts/kotlinc.xml
|
||||
.idea/artifacts/kotlin_compiler_jar.xml
|
||||
.idea/artifacts/kotlin_plugin_jar.xml
|
||||
.idea/artifacts/kotlin_jps_plugin_jar.xml
|
||||
.idea/artifacts/kotlin_daemon_client_jar.xml
|
||||
.idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml
|
||||
.idea/artifacts/kotlin_main_kts_jar.xml
|
||||
.idea/artifacts/kotlin_compiler_client_embeddable_jar.xml
|
||||
.idea/artifacts/kotlin_reflect_jar.xml
|
||||
.idea/artifacts/kotlin_stdlib_js_ir_*
|
||||
.idea/artifacts/kotlin_test_js_ir_*
|
||||
.idea/artifacts/kotlin_stdlib_wasm_*
|
||||
.idea/jarRepositories.xml
|
||||
.idea/csv-plugin.xml
|
||||
.idea/libraries-with-intellij-classes.xml
|
||||
node_modules/
|
||||
.rpt2_cache/
|
||||
libraries/tools/kotlin-test-js-runner/lib/
|
||||
local.properties
|
||||
buildSrcTmp/
|
||||
distTmp/
|
||||
outTmp/
|
||||
/test.output
|
||||
/kotlin-native/dist
|
||||
kotlin-ide/
|
|
@ -0,0 +1,9 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,71 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'androidx.navigation.safeargs.kotlin'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.tarkvaraprojekt.mobileauthapp"
|
||||
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 {
|
||||
def lifecycle_version = "2.3.1"
|
||||
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.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
//To use activityViewModels
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
//For navigation component
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
//ViewModel
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
|
||||
//For cryptography
|
||||
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69'
|
||||
|
||||
//SecureDataStoring
|
||||
implementation("androidx.security:security-crypto:1.0.0")
|
||||
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
|
||||
'org.bouncycastle:bcprov-jdk15on:1.60',
|
||||
'io.jsonwebtoken:jjwt-gson:0.11.2'
|
||||
|
||||
implementation 'com.koushikdutta.ion:ion:3.1.0'
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,24 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
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.tarkvaraprojekt.mobileauthapp", appContext.packageName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.tarkvaraprojekt.mobileauthapp">
|
||||
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<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.MobileAuthApp">
|
||||
<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>
|
||||
<!-- for launching the app with deep links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="authapp" android:host="start" android:path="/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,160 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import java.lang.Exception
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Fragment that asks the user to detect the ID card with mobile NFC chip.
|
||||
* Currently contains a next button that won't be needed later on.
|
||||
* This button is just needed to test navigation between fragments so that every step exists.
|
||||
*/
|
||||
class AuthFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private val paramsModel: ParametersViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentAuthBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val args: CanFragmentArgs by navArgs()
|
||||
|
||||
private lateinit var timer: CountDownTimer
|
||||
|
||||
private var timeRemaining: Int = 90
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentAuthBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
timer = object : CountDownTimer((timeRemaining * 1000).toLong(), 1000) {
|
||||
override fun onTick(p0: Long) {
|
||||
timeRemaining--
|
||||
if (timeRemaining == 0) {
|
||||
binding.timeCounter.text = getString(R.string.no_time)
|
||||
} else {
|
||||
binding.timeCounter.text = getString(R.string.time_left, timeRemaining)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
Thread.sleep(750)
|
||||
cancelAuth(408)
|
||||
}
|
||||
}.start()
|
||||
// The button exists in code for testing reasons, but not visible to the user anymore unless visibility is changed in the code.
|
||||
binding.nextButton.visibility = View.GONE
|
||||
binding.nextButton.setOnClickListener { goToNextFragment() }
|
||||
binding.cancelButton.setOnClickListener { cancelAuth(444) }
|
||||
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||
if (adapter != null)
|
||||
getInfoFromIdCard(adapter)
|
||||
else { // If NFC adapter can not be detected then end the auth process as it is not possible to read an ID card
|
||||
cancelAuth(447) // It would be a good idea to show user some notification as it might be confusing if the app suddenly closes
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToNextFragment() {
|
||||
timer.cancel()
|
||||
val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun cancelAuth(code: Int) {
|
||||
viewModel.clearUserInfo()
|
||||
timer.cancel()
|
||||
if (args.mobile) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
(activity as MainActivity).returnError(code)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
||||
adapter.enableReaderMode(activity, { tag ->
|
||||
timer.cancel()
|
||||
requireActivity().runOnUiThread {
|
||||
binding.timeCounter.text = getString(R.string.card_detected)
|
||||
}
|
||||
val card = IsoDep.get(tag)
|
||||
card.timeout = 32768
|
||||
card.use {
|
||||
try {
|
||||
val comms = Comms(it, viewModel.userCan)
|
||||
val jws = Authenticator(comms).authenticate(
|
||||
paramsModel.challenge,
|
||||
paramsModel.origin,
|
||||
viewModel.userPin
|
||||
)
|
||||
paramsModel.setToken(jws)
|
||||
requireActivity().runOnUiThread {
|
||||
goToNextFragment()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when(e) {
|
||||
is TagLostException -> requireActivity().runOnUiThread {
|
||||
binding!!.timeCounter.text = getString(R.string.id_card_removed_early)
|
||||
cancelAuth(444)
|
||||
}
|
||||
else -> {
|
||||
when ("invalid pin") {
|
||||
in e.message.toString().lowercase() -> requireActivity().runOnUiThread {
|
||||
val messagePieces = e.message.toString().split(" ")
|
||||
binding.timeCounter.text = getString(R.string.wrong_pin, messagePieces[messagePieces.size - 1])
|
||||
viewModel.deletePin(requireContext())
|
||||
cancelAuth(449)
|
||||
}
|
||||
else -> requireActivity().runOnUiThread {
|
||||
binding.timeCounter.text = getString(R.string.wrong_can_text)
|
||||
viewModel.deleteCan(requireContext())
|
||||
cancelAuth(449)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Give user some time to read the error message
|
||||
Thread.sleep(2000)
|
||||
} finally {
|
||||
adapter.disableReaderMode(activity)
|
||||
}
|
||||
}
|
||||
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentCanBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import org.w3c.dom.Text
|
||||
|
||||
/**
|
||||
* Fragment that deals with asking the user for a six digit CAN. If the CAN is already saved
|
||||
* then the fragment is skipped immediately and if the CAN is not saved then the user
|
||||
* is asked whether it should be saved for the future or not before continuing.
|
||||
*/
|
||||
class CanFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentCanBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// Navigation arguments:
|
||||
// saving = true means that we are navigating here from the settings menu and must return to the settings menu.
|
||||
private val args: CanFragmentArgs by navArgs()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentCanBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
checkIfSkip()
|
||||
binding.canTextField.editText?.addTextChangedListener {
|
||||
checkEnteredCan()
|
||||
}
|
||||
binding.buttonCancel.setOnClickListener { goToTheStart() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current fragment can be skipped or not.
|
||||
* If the user has CAN saved on the device there is no need to ask it again.
|
||||
*/
|
||||
private fun checkIfSkip() {
|
||||
if (viewModel.userCan.length == 6) {
|
||||
goToTheNextFragment()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes user to the next fragment, which is PinFragment.
|
||||
*/
|
||||
private fun goToTheNextFragment() {
|
||||
val action = CanFragmentDirections.actionCanFragmentToPinFragment(auth = args.auth, mobile = args.mobile)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates the user back to the start depending on where the user arrived.
|
||||
* If the user arrived from the settings menu then the start is the settings menu
|
||||
* not the HomeFragment.
|
||||
*/
|
||||
private fun goToTheStart() {
|
||||
if (args.saving) {
|
||||
if (args.fromhome) {
|
||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
|
||||
}
|
||||
} else if (args.auth || args.mobile) {
|
||||
if (args.mobile) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
(activity as MainActivity).returnError(444)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates and shows a snackbar that tells the user that CAN has been saved
|
||||
*/
|
||||
private fun showSnackbar() {
|
||||
val snackbar = Snackbar.make(requireView(), R.string.can_status_saved, Snackbar.LENGTH_SHORT)
|
||||
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
|
||||
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has entered a 6 digit can to the input field.
|
||||
* If yes then the user is allowed to continue otherwise the user is
|
||||
* allowed to modify the entered can.
|
||||
*/
|
||||
private fun checkEnteredCan() {
|
||||
val enteredCan = binding.canTextField.editText?.text.toString()
|
||||
if (enteredCan.length == 6) {
|
||||
viewModel.setUserCan(enteredCan)
|
||||
viewModel.storeCan(requireContext()) //Maybe storeCan should always automatically call setUserCan method as well because these methods usually are used together
|
||||
showSnackbar()
|
||||
if (args.saving) {
|
||||
goToTheStart()
|
||||
} else {
|
||||
goToTheNextFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import org.json.JSONObject
|
||||
import java.lang.Exception
|
||||
import java.lang.RuntimeException
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* HomeFragment is only shown to the user when then the user launches the application. When the application
|
||||
* is launched by another application or a website then this Fragment will be skipped.
|
||||
* This fragment uses the fields from the MainActivity by casting the activity to MainActivity.
|
||||
* This might not be the best practice, but the application uses a single activity design so it should
|
||||
* always work.
|
||||
*/
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private val intentParams: ParametersViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// The ID card reader mode is enabled on the home fragment when can is saved.
|
||||
private var canSaved: Boolean = false
|
||||
|
||||
// Is the app used for authentication
|
||||
private var auth: Boolean = false
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
// Making settings menu active again
|
||||
(activity as MainActivity).menuAvailable = true
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initialChecks()
|
||||
if (requireActivity().intent.data?.getQueryParameter("action") != null) {
|
||||
// Currently we only support authentication not signing.
|
||||
auth = true
|
||||
}
|
||||
val mobile = requireActivity().intent.getBooleanExtra("mobile", false)
|
||||
if (auth || mobile) {
|
||||
startAuthentication(mobile)
|
||||
} else {
|
||||
receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(p0: Context?, p1: Intent?) {
|
||||
updateAction(canSaved)
|
||||
}
|
||||
}
|
||||
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
|
||||
requireActivity().registerReceiver(receiver, filter)
|
||||
updateAction(canSaved)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process of interacting with the ID card by sending user to the CAN fragment.
|
||||
*/
|
||||
private fun goToTheNextFragment(mobile: Boolean = false) {
|
||||
(activity as MainActivity).menuAvailable = false
|
||||
val action =
|
||||
HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that starts the authentication use case.
|
||||
*
|
||||
* NOTE: Comment out try-catch block when testing without backend
|
||||
*/
|
||||
private fun startAuthentication(mobile: Boolean) {
|
||||
try {
|
||||
if (mobile) {
|
||||
// We use !! to get extras because we want an exception to be thrown when something is missing.
|
||||
//intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
||||
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
|
||||
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
|
||||
val challengeUrl = requireActivity().intent.getStringExtra("challenge")!!
|
||||
val headers = requireActivity().intent.getStringExtra("headers")!!
|
||||
val map: HashMap<String, String> = HashMap()
|
||||
map.put("sessionId", headers)
|
||||
intentParams.setHeaders(map)
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
Ion.with(activity)
|
||||
.load(challengeUrl)
|
||||
.setHeader("sessionId", headers)
|
||||
.asJsonObject()
|
||||
.setCallback { _, result ->
|
||||
try {
|
||||
val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
|
||||
intentParams.setChallenge(challenge)
|
||||
goToTheNextFragment(mobile)
|
||||
} catch (e: Exception) {
|
||||
Log.i("GETrequest", e.toString())
|
||||
}
|
||||
}
|
||||
} else { //Website
|
||||
/*
|
||||
var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!!
|
||||
// TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back.
|
||||
challenge = challenge.replace(" ", "+")
|
||||
intentParams.setChallenge(challenge)
|
||||
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
|
||||
intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!)
|
||||
*/
|
||||
var getAuthChallengeUrl =
|
||||
requireActivity().intent.data!!.getQueryParameter("getAuthChallengeUrl")!!
|
||||
getAuthChallengeUrl =
|
||||
getAuthChallengeUrl.substring(1, getAuthChallengeUrl.length - 1)
|
||||
var postAuthTokenUrl =
|
||||
requireActivity().intent.data!!.getQueryParameter("postAuthTokenUrl")!!
|
||||
postAuthTokenUrl = postAuthTokenUrl.substring(1, postAuthTokenUrl.length - 1)
|
||||
val headers =
|
||||
getHeaders(requireActivity().intent.data!!.getQueryParameter("headers")!!)
|
||||
intentParams.setAuthUrl(postAuthTokenUrl)
|
||||
val address = "https://" + URL(getAuthChallengeUrl).host
|
||||
intentParams.setOrigin(address)
|
||||
intentParams.setHeaders(headers)
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
val ion = Ion.with(activity)
|
||||
.load(getAuthChallengeUrl)
|
||||
|
||||
// Set headers.
|
||||
for ((header, value) in intentParams.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
.asJsonObject()
|
||||
.setCallback { _, result ->
|
||||
try {
|
||||
// Get data from the result and call launchAuth method
|
||||
val challenge =
|
||||
result.asJsonObject["nonce"].toString().replace("\"", "")
|
||||
intentParams.setChallenge(challenge)
|
||||
goToTheNextFragment(mobile)
|
||||
} catch (e: Exception) {
|
||||
Log.i("GETrequest", "was unsuccessful" + e.message)
|
||||
throw RuntimeException()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// There was a problem with parameters, which means that authentication is not possible.
|
||||
// In that case we will cancel the authentication immediately as it would be waste of the user's time to carry on
|
||||
// before getting an inevitable error.
|
||||
val message = MaterialAlertDialogBuilder(requireContext())
|
||||
message.setTitle(getString(R.string.problem_parameters))
|
||||
if (intentParams.challenge == "") {
|
||||
message.setMessage(getString(R.string.problem_challenge))
|
||||
} else if (intentParams.authUrl == "") {
|
||||
message.setMessage(getString(R.string.problem_authurl))
|
||||
} else if (intentParams.origin == "") {
|
||||
message.setMessage(getString(R.string.problem_originurl))
|
||||
} else {
|
||||
message.setMessage(getString(R.string.problem_other))
|
||||
}
|
||||
message.setPositiveButton(getString(R.string.continue_button)) { _, _ ->
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
message.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the state of the CAN, saved or not saved. Updates the text and logo.
|
||||
*/
|
||||
private fun canState() {
|
||||
if (viewModel.userCan.length == 6) {
|
||||
binding.canStatusText.text = getString(R.string.can_status_saved)
|
||||
binding.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
|
||||
canSaved = true
|
||||
} else {
|
||||
binding.canStatusText.text = getString(R.string.can_status_negative)
|
||||
binding.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
|
||||
canSaved = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the state of the PIN 1, saved or not saved. Updates the text and logo.
|
||||
*/
|
||||
private fun pinState() {
|
||||
if (viewModel.userPin.length in 4..12) {
|
||||
binding.pinStatusText.text = getString(R.string.pin_status_saved)
|
||||
binding.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
|
||||
} else {
|
||||
binding.pinStatusText.text = getString(R.string.pin_status_negative)
|
||||
binding.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHeaders(headersString: String): Map<String, String> {
|
||||
val headers = HashMap<String, String>()
|
||||
val headersStringFormatted = headersString.substring(1, headersString.length - 1)
|
||||
val headersJsonObject = JSONObject(headersStringFormatted)
|
||||
|
||||
for (name in headersJsonObject.keys()) {
|
||||
headers[name] = headersJsonObject[name].toString()
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays texts that inform the user whether the CAN and PIN 1 are saved on the device or not.
|
||||
* This might help the user to save some time as checking menu is not necessary unless the user
|
||||
* wishes to make changes to the saved CAN or PIN 1.
|
||||
*/
|
||||
private fun displayStates() {
|
||||
canState()
|
||||
pinState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method where all the initial checks that should be completed before any user input is accepted should be conducted.
|
||||
*/
|
||||
private fun initialChecks() {
|
||||
viewModel.checkCan(requireContext())
|
||||
viewModel.checkPin(requireContext())
|
||||
displayStates()
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a help message to the user explaining what the CAN is
|
||||
*/
|
||||
private fun displayMessage(title: String, message: String) {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.return_text) { _, _ -> }
|
||||
.show()
|
||||
val title = dialog.findViewById<TextView>(R.id.alertTitle)
|
||||
title?.textSize = 24F
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs user whether the ID card can be detected or not.
|
||||
*/
|
||||
private fun updateAction(canIsSaved: Boolean) {
|
||||
if (canIsSaved) {
|
||||
binding.detectionActionText.text = getString(R.string.action_detect)
|
||||
enableReaderMode()
|
||||
binding.homeActionButton.visibility = View.GONE
|
||||
binding.homeHelpButton.visibility = View.GONE
|
||||
} else {
|
||||
binding.detectionActionText.text = getString(R.string.action_detect_unavailable)
|
||||
binding.homeActionButton.text = getString(R.string.add_can_text)
|
||||
binding.homeActionButton.setOnClickListener {
|
||||
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(
|
||||
saving = true,
|
||||
fromhome = true
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
binding.homeHelpButton.setOnClickListener {
|
||||
displayMessage(
|
||||
getString(R.string.can_question),
|
||||
getString(R.string.can_explanation)
|
||||
)
|
||||
}
|
||||
binding.homeActionButton.visibility = View.VISIBLE
|
||||
binding.homeHelpButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the error message and allows the user to try again
|
||||
*/
|
||||
private fun reset() {
|
||||
binding.homeActionButton.text = getString(R.string.try_again_text)
|
||||
binding.homeActionButton.setOnClickListener {
|
||||
updateAction(canSaved)
|
||||
}
|
||||
binding.homeActionButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that enables the NFC reader mode, which allows the app to communicate with the ID card and retrieve information.
|
||||
*/
|
||||
private fun enableReaderMode() {
|
||||
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||
if (adapter == null || !adapter.isEnabled) {
|
||||
binding.detectionActionText.text = getString(R.string.nfc_not_available)
|
||||
} else {
|
||||
adapter.enableReaderMode(activity, { tag ->
|
||||
requireActivity().runOnUiThread {
|
||||
binding.detectionActionText.text = getString(R.string.card_detected)
|
||||
}
|
||||
val card = IsoDep.get(tag)
|
||||
card.timeout = 32768
|
||||
card.use {
|
||||
try {
|
||||
val comms = Comms(it, viewModel.userCan)
|
||||
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
|
||||
viewModel.setUserFirstName(response[1])
|
||||
viewModel.setUserLastName(response[0])
|
||||
viewModel.setUserIdentificationNumber(response[2])
|
||||
viewModel.setGender(response[3])
|
||||
viewModel.setCitizenship(response[4])
|
||||
viewModel.setExpiration(response[5])
|
||||
requireActivity().runOnUiThread {
|
||||
val action = HomeFragmentDirections.actionHomeFragmentToUserFragment()
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is TagLostException -> requireActivity().runOnUiThread {
|
||||
binding.detectionActionText.text =
|
||||
getString(R.string.id_card_removed_early)
|
||||
reset()
|
||||
}
|
||||
else -> requireActivity().runOnUiThread {
|
||||
binding.detectionActionText.text =
|
||||
getString(R.string.nfc_reading_error)
|
||||
viewModel.deleteCan(requireContext())
|
||||
canState()
|
||||
reset()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
adapter.disableReaderMode(activity)
|
||||
}
|
||||
}
|
||||
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
if (receiver != null) {
|
||||
requireActivity().unregisterReceiver(receiver)
|
||||
}
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navArgs
|
||||
import com.google.gson.JsonObject
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
|
||||
|
||||
/**
|
||||
* The only activity of the application (single activity design).
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var navigationController: NavController
|
||||
private val paramsModel: ParametersViewModel by viewModels()
|
||||
|
||||
|
||||
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
|
||||
var menuAvailable: Boolean = true
|
||||
|
||||
var inMenu: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
navigationController = navHostFragment.navController
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.menu_settings_option -> {
|
||||
if (menuAvailable) {
|
||||
navigationController.navigate(R.id.action_homeFragment_to_settingsFragment)
|
||||
menuAvailable = false
|
||||
inMenu = true
|
||||
true
|
||||
} else {
|
||||
if (!inMenu) {
|
||||
Toast.makeText(this, getString(R.string.menu_unavailable_message), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun returnError(errorCode: Int) {
|
||||
val json = JsonObject()
|
||||
json.addProperty("auth-token", "")
|
||||
json.addProperty("error", errorCode)
|
||||
|
||||
Ion.getDefault(this).conscryptMiddleware.enable(false)
|
||||
val ion = Ion.with(this)
|
||||
.load(paramsModel.authUrl)
|
||||
for ((header, value) in paramsModel.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,441 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp.NFC;
|
||||
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.crypto.BlockCipher;
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.macs.CMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Comms {
|
||||
|
||||
private static final byte[] selectMaster = Hex.decode("00a4040c10a000000077010800070000fe00000100");
|
||||
|
||||
private static final byte[] MSESetAT = Hex.decode("0022c1a40f800a04007f0007020204020483010200");
|
||||
|
||||
private static final byte[] GAGetNonce = Hex.decode("10860000027c0000");
|
||||
|
||||
private static final byte[] GAMapNonceIncomplete = Hex.decode("10860000457c438141");
|
||||
|
||||
private static final byte[] GAKeyAgreementIncomplete = Hex.decode("10860000457c438341");
|
||||
|
||||
private static final byte[] GAMutualAuthenticationIncomplete = Hex.decode("008600000c7c0a8508");
|
||||
|
||||
private static final byte[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641");
|
||||
|
||||
private static final byte[] selectFile = Hex.decode("0ca4010c1d871101");
|
||||
|
||||
private static final byte[] readFile = Hex.decode("0cb000000d970100");
|
||||
|
||||
private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101");
|
||||
|
||||
private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101");
|
||||
|
||||
private static final byte[] MSESetEnv = Hex.decode("0c2241A41d871101");
|
||||
|
||||
private static final byte[] Env = Hex.decode("8004FF200800840181");
|
||||
|
||||
private static final byte[] InternalAuthenticate = Hex.decode("0c8800001d871101");
|
||||
|
||||
private static final byte[] IASECCFID = {0x3f, 0x00};
|
||||
private static final byte[] personalDF = {0x50, 0x00};
|
||||
private static final byte[] AWP = {(byte) 0xad, (byte) 0xf1};
|
||||
private static final byte[] QSCD = {(byte) 0xad, (byte) 0xf2};
|
||||
private static final byte[] authCert = {0x34, 0x01};
|
||||
private static final byte[] signCert = {0x34, 0x1f};
|
||||
|
||||
private final IsoDep idCard;
|
||||
private final byte[] keyEnc;
|
||||
private final byte[] keyMAC;
|
||||
private byte ssc; // Send sequence counter.
|
||||
|
||||
/**
|
||||
* The constructor performs PACE and stores the session keys
|
||||
*
|
||||
* @param idCard link to the card
|
||||
* @param CAN the card authentication number
|
||||
*/
|
||||
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||
|
||||
idCard.connect();
|
||||
this.idCard = idCard;
|
||||
byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8));
|
||||
keyEnc = keys[0];
|
||||
keyMAC = keys[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the message authentication code
|
||||
*
|
||||
* @param APDU the byte array on which the CMAC algorithm is performed
|
||||
* @param keyMAC the key for performing CMAC
|
||||
* @return MAC
|
||||
*/
|
||||
private byte[] getMAC(byte[] APDU, byte[] keyMAC) {
|
||||
BlockCipher blockCipher = new AESEngine();
|
||||
CMac cmac = new CMac(blockCipher);
|
||||
cmac.init(new KeyParameter(keyMAC));
|
||||
cmac.update(APDU, 0, APDU.length);
|
||||
byte[] MAC = new byte[cmac.getMacSize()];
|
||||
cmac.doFinal(MAC, 0);
|
||||
return Arrays.copyOf(MAC, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an application protocol data unit
|
||||
*
|
||||
* @param template the byte array to be used as a template
|
||||
* @param data the data necessary for completing the APDU
|
||||
* @param extra the missing length of the APDU being created
|
||||
* @return the complete APDU
|
||||
*/
|
||||
private byte[] createAPDU(byte[] template, byte[] data, int extra) {
|
||||
byte[] APDU = Arrays.copyOf(template, template.length + extra);
|
||||
System.arraycopy(data, 0, APDU, template.length, data.length);
|
||||
return APDU;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cipher key
|
||||
*
|
||||
* @param unpadded the array to be used as the basis for the key
|
||||
* @param last the last byte in the appended padding
|
||||
* @return the constructed key
|
||||
*/
|
||||
private byte[] createKey(byte[] unpadded, byte last) throws NoSuchAlgorithmException {
|
||||
byte[] padded = Arrays.copyOf(unpadded, unpadded.length + 4);
|
||||
padded[padded.length - 1] = last;
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
return messageDigest.digest(padded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the nonce
|
||||
*
|
||||
* @param encryptedNonce the encrypted nonce received from the chip
|
||||
* @param CAN the card access number provided by the user
|
||||
* @return the decrypted nonce
|
||||
*/
|
||||
private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||
byte[] decryptionKey = createKey(CAN, (byte) 3);
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||
return cipher.doFinal(encryptedNonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Communicates with the card and logs the response
|
||||
*
|
||||
* @param APDU The command
|
||||
* @param log Information for logging
|
||||
* @return The response
|
||||
*/
|
||||
private byte[] getResponse(byte[] APDU, String log) throws IOException {
|
||||
byte[] response = idCard.transceive(APDU);
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException(String.format("%s failed.", log));
|
||||
}
|
||||
Log.i(log, Hex.toHexString(response));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
|
||||
*
|
||||
* @param CAN the card access number
|
||||
*/
|
||||
private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
||||
|
||||
// select the IAS-ECC application on the chip
|
||||
getResponse(selectMaster, "Select the master application");
|
||||
|
||||
// initiate PACE
|
||||
getResponse(MSESetAT, "Set authentication template");
|
||||
|
||||
// get nonce
|
||||
byte[] response = getResponse(GAGetNonce, "Get nonce");
|
||||
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
|
||||
|
||||
// generate an EC keypair and exchange public keys with the chip
|
||||
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
|
||||
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
|
||||
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
|
||||
response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce");
|
||||
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||
|
||||
// calculate the new base point, use it to generate a new keypair, and exchange public keys
|
||||
ECPoint sharedSecret = cardPublicKey.multiply(privateKey);
|
||||
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
|
||||
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
|
||||
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
|
||||
response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement");
|
||||
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||
|
||||
// generate the session keys and exchange MACs to verify them
|
||||
byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
|
||||
byte[] keyEnc = createKey(secret, (byte) 1);
|
||||
byte[] keyMAC = createKey(secret, (byte) 2);
|
||||
byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
|
||||
response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
|
||||
|
||||
// verify chip's MAC and return session keys
|
||||
MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
|
||||
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
|
||||
throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen.
|
||||
}
|
||||
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
|
||||
*
|
||||
* @param data the array containing the data to be processed
|
||||
* @param mode indicates whether to en- or decrypt the data
|
||||
* @return the result of encryption or decryption
|
||||
*/
|
||||
private byte[] encryptDecryptData(byte[] data, int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(keyEnc, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
|
||||
byte[] iv = Arrays.copyOf(cipher.doFinal(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ssc}), 16);
|
||||
cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
cipher.init(mode, secretKeySpec, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs APDUs suitable for the secure channel.
|
||||
*
|
||||
* @param data the data to be encrypted
|
||||
* @param incomplete the array to be used as a template
|
||||
* @return the constructed APDU
|
||||
*/
|
||||
private byte[] createSecureAPDU(byte[] data, byte[] incomplete) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
||||
|
||||
ssc++;
|
||||
byte[] encryptedData = new byte[0];
|
||||
int length = 16 * (1 + data.length / 16);
|
||||
|
||||
// construct the required array and calculate the MAC based on it
|
||||
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
|
||||
macData[15] = ssc; // first block contains the ssc
|
||||
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
|
||||
macData[20] = (byte) 0x80; // elements are terminated by 0x80 and zero-padded to the next block
|
||||
System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le
|
||||
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
|
||||
byte[] paddedData = Arrays.copyOf(data, length);
|
||||
paddedData[data.length] = (byte) 0x80;
|
||||
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
|
||||
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
|
||||
}
|
||||
macData[35 + encryptedData.length] = (byte) 0x80;
|
||||
byte[] MAC = getMAC(macData, keyMAC);
|
||||
|
||||
// construct the APDU using the encrypted data and the MAC
|
||||
byte[] APDU = Arrays.copyOf(incomplete, incomplete.length + encryptedData.length + MAC.length + 3);
|
||||
if (encryptedData.length > 0) {
|
||||
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
|
||||
}
|
||||
System.arraycopy(new byte[]{(byte) 0x8E, 0x08}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
|
||||
System.arraycopy(MAC, 0, APDU, incomplete.length + encryptedData.length + 2, MAC.length);
|
||||
ssc++;
|
||||
return APDU;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a FILE by its identifier
|
||||
*
|
||||
*/
|
||||
private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
byte[] response = getResponse(FID, selectFile, String.format("Select %s", info));
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException(String.format("Could not select %s", info));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the personal data dedicated file
|
||||
*
|
||||
* @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16)
|
||||
* @return array containing the corresponding data strings
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPin2Binding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
|
||||
/**
|
||||
* Fragment that deals with asking PIN 2 from the user. Basically the same as PIN 1 fragment.
|
||||
*/
|
||||
class Pin2Fragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentPin2Binding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentPin2Binding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.nextButton.setOnClickListener {
|
||||
checkPin2Length()
|
||||
}
|
||||
binding.cancelButton.setOnClickListener {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the length of the entered PIN 2 is in range 5..12 and if it is
|
||||
* then it is saved to the viewModel.
|
||||
*/
|
||||
private fun checkPin2Length() {
|
||||
val enteredPin2 = binding.pin2EditText.editText?.text.toString()
|
||||
if (enteredPin2.length in 5..12) {
|
||||
viewModel.setUserPin2(enteredPin2)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), getString(R.string.length_pin2), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication process is cancelled when cancel button is clicked and the application
|
||||
* will be closed.
|
||||
*/
|
||||
private fun cancel() {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentPinBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
|
||||
/**
|
||||
* Fragment that deals with asking the user for PIN 1. If the user has already saved the PIN 1 then it is not asked again
|
||||
* and the fragment is skipped and if the PIN 1 is not saved then the user is asked whether it should be saved or
|
||||
* not before continuing.
|
||||
*/
|
||||
class PinFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentPinBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// Navigation arguments:
|
||||
// saving = true means that the user must be returned to the settings menu
|
||||
private val args: PinFragmentArgs by navArgs()
|
||||
|
||||
private var saveToggle = true
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentPinBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
checkIfSkip()
|
||||
// Switch should be not visible when user is in savings mode
|
||||
if (args.saving) {
|
||||
binding.savePinQuestion.visibility = View.GONE
|
||||
binding.saveLayout.visibility = View.GONE
|
||||
} else {
|
||||
saveToggle =
|
||||
activity?.getPreferences(Context.MODE_PRIVATE)?.getBoolean("saveToggle", true) == true //Android Studio recommendation to get rid of Boolean?.
|
||||
if (!saveToggle) {
|
||||
binding.saveSwitch.isChecked = false
|
||||
}
|
||||
binding.saveSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
binding.saveStatus.text = getString(R.string.pin_save_on)
|
||||
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", true)?.apply()
|
||||
} else {
|
||||
binding.saveStatus.text = getString(R.string.pin_save_off)
|
||||
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", false)?.apply()
|
||||
}
|
||||
saveToggle = !saveToggle
|
||||
}
|
||||
}
|
||||
binding.buttonContinue.setOnClickListener { checkEnteredPin() }
|
||||
binding.buttonCancel.setOnClickListener { goToTheStart() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes user to the next fragment, which is AuthFragment.
|
||||
*/
|
||||
private fun goToTheNextFragment() {
|
||||
val action = PinFragmentDirections.actionPinFragmentToAuthFragment(auth = args.auth, mobile = args.mobile)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns user to the start. If the user arrived from the settings menu then the start is
|
||||
* settings menu not the HomeFragment.
|
||||
*/
|
||||
private fun goToTheStart() {
|
||||
if (args.saving) {
|
||||
findNavController().navigate(R.id.action_pinFragment_to_settingsFragment)
|
||||
} else if (args.auth || args.mobile) {
|
||||
if (args.mobile) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
(activity as MainActivity).returnError(444)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current fragment can be skipped or not.
|
||||
* If the user has PIN 1 saved on the device or PIN 1 is not required
|
||||
* then the PIN 1 won't be asked.
|
||||
*/
|
||||
private fun checkIfSkip() {
|
||||
if (viewModel.userPin.length in 4..12) {
|
||||
goToTheNextFragment()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates and shows a snackbar that tells the user that PIN 1 has been saved
|
||||
*/
|
||||
private fun showSnackbar() {
|
||||
val snackbar = Snackbar.make(requireView(), R.string.pin_status_saved, Snackbar.LENGTH_SHORT)
|
||||
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
|
||||
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has entered a PIN 1 with length between [4, 12] in the
|
||||
* input field. If yes then the user is allowed to continue otherwise the user is
|
||||
* allowed to modify the entered PIN 1.
|
||||
*/
|
||||
private fun checkEnteredPin() {
|
||||
val enteredPin = binding.pinTextField.editText?.text.toString()
|
||||
if (enteredPin.length in 4..12) {
|
||||
viewModel.setUserPin(enteredPin)
|
||||
if (args.saving) {
|
||||
viewModel.storePin(requireContext())
|
||||
showSnackbar()
|
||||
goToTheStart()
|
||||
} else {
|
||||
if (saveToggle) {
|
||||
viewModel.storePin(requireContext())
|
||||
showSnackbar()
|
||||
}
|
||||
goToTheNextFragment()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(requireContext(), getString(R.string.pin_helper_text), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* ResultFragment is used to create a JWT and to send response to the website/application
|
||||
* that launched the MobileAuthApp. If the mobile auth app was started by a website
|
||||
* the result is sent to a server with a POST request.
|
||||
*/
|
||||
class ResultFragment : Fragment() {
|
||||
|
||||
private val paramsModel: ParametersViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentResultBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val args: ResultFragmentArgs by navArgs()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentResultBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
postToken()
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used when the MobileAuthApp was launched by an app. Not for website use.
|
||||
* Not really the safest way of doing things, but sufficient for POC purposes.
|
||||
*/
|
||||
private fun createResponse(
|
||||
success: Boolean = true,
|
||||
idCode: String = "noCode",
|
||||
name: String = "noName",
|
||||
authority: String = "noAuthority"
|
||||
) {
|
||||
val responseCode =
|
||||
if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("idCode", idCode)
|
||||
resultIntent.putExtra("name", name)
|
||||
resultIntent.putExtra("authority", authority)
|
||||
requireActivity().setResult(responseCode, resultIntent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a POST request to the backend server with a tokenItem
|
||||
*/
|
||||
fun postToken() {
|
||||
val json = JsonObject()
|
||||
json.addProperty("auth-token", paramsModel.token)
|
||||
json.addProperty("error", 200)
|
||||
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
val ion = Ion.with(activity)
|
||||
.load(paramsModel.authUrl)
|
||||
for ((header, value) in paramsModel.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { e, result ->
|
||||
Log.i("resultTag", result.toString())
|
||||
if (result == null) {
|
||||
if (args.mobile) {
|
||||
createResponse(false)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
if (args.mobile) {
|
||||
val userData = result.asJsonObject["userData"]
|
||||
val idCode = userData.asJsonObject["idCode"].asString
|
||||
val name = userData.asJsonObject["name"].asString
|
||||
val authority = result.asJsonObject["roles"].asJsonArray[0].asJsonObject["authority"].asString
|
||||
createResponse(true, idCode, name, authority)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentUserBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
|
||||
/**
|
||||
* Fragment that is used to display the persons name and national identification number.
|
||||
* Currently needed in order to test that the app is working and information is read
|
||||
* from the ID card via NFC.
|
||||
*/
|
||||
class UserFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentUserBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentUserBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
displayInformation()
|
||||
binding.clearButton.setOnClickListener { goToTheStart() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns text values to the fields in order to display user information.
|
||||
*/
|
||||
private fun displayInformation() {
|
||||
binding.userName.text =
|
||||
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
|
||||
binding.identificationNumber.text = viewModel.userIdentificationNumber
|
||||
binding.gender.text = viewModel.gender
|
||||
binding.expiration.text = viewModel.expiration.replace(" ", "/")
|
||||
binding.citizenship.text = viewModel.citizenship
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates user back to the start and also deletes any temporary information.
|
||||
*/
|
||||
private fun goToTheStart() {
|
||||
viewModel.clearUserInfo()
|
||||
findNavController().navigate(R.id.action_userFragment_to_homeFragment)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp.auth
|
||||
|
||||
import android.util.Log
|
||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||
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) {
|
||||
|
||||
val type = "JWT"
|
||||
val algorithm = "ES384"
|
||||
var iss = "https://self-issued.me" // Will be specified at a later date.
|
||||
val algorithmUsedForSigning = SignatureAlgorithm.ES384
|
||||
|
||||
fun authenticate(challenge: String, originUrl: String, pin1: String): String {
|
||||
|
||||
// Ask PIN 1 from the user and get the authentication certificate from the ID card.
|
||||
val authenticationCertificate: ByteArray = comms.getCertificate(true);
|
||||
|
||||
// Encode the certificate in base64.
|
||||
val base64cert = java.util.Base64.getEncoder().encodeToString(authenticationCertificate)
|
||||
|
||||
// Get current epoch time.
|
||||
val epoch = LocalDateTime.now(ZoneOffset.UTC).atZone(ZoneOffset.UTC).toEpochSecond()
|
||||
|
||||
// Get expiration time.
|
||||
val exp = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(5 * 60L).atZone(ZoneOffset.UTC)
|
||||
.toEpochSecond()
|
||||
|
||||
// TODO: Get subject value.
|
||||
val sub = "FAMILYNAME.NAME"
|
||||
|
||||
// Get header and claims.
|
||||
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
|
||||
val claims =
|
||||
"""{"iat":"$epoch","exp":"$exp","aud":["$originUrl"],"iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
||||
|
||||
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
|
||||
claims.toByteArray(Charsets.UTF_8)
|
||||
)
|
||||
|
||||
// Send the authentication token hash to the ID card for signing and get signed authentication token as response.
|
||||
val encoded =
|
||||
MessageDigest.getInstance("SHA-384").digest(jwt.toByteArray(StandardCharsets.UTF_8))
|
||||
val signed = comms.authenticate(pin1, encoded)
|
||||
|
||||
// Return the signed authentication token.
|
||||
return jwt + "." + base64Encode(signed)
|
||||
}
|
||||
|
||||
fun base64Encode(bytes: ByteArray): String? {
|
||||
val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes)
|
||||
return encoded.replace("=", "")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp.menu
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tarkvaraprojekt.mobileauthapp.MainActivity
|
||||
import com.tarkvaraprojekt.mobileauthapp.R
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentSettingsBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
|
||||
/**
|
||||
* This fragment allows the user to save the CAN and the PIN 1 and also to delete them if necessary.
|
||||
* Should only be accessible for the user from the HomeFragment and not during the
|
||||
* authentication process itself.
|
||||
*/
|
||||
class SettingsFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private var _binding: FragmentSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var showPin: Boolean = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
showCanField()
|
||||
showPinField()
|
||||
togglePinButton()
|
||||
binding.canMenuAction.setOnClickListener { canAction() }
|
||||
binding.pinMenuAction.setOnClickListener { pinAction() }
|
||||
binding.pinMenuShow.setOnClickListener { togglePin() }
|
||||
binding.returnButton.setOnClickListener { backToHome() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for showing a snackbar with a message that is given as a parameter
|
||||
*/
|
||||
private fun showSnackbar(message: String) {
|
||||
val snackbar = Snackbar.make(requireView(), message, Snackbar.LENGTH_SHORT)
|
||||
val snackbarText: TextView = snackbar.view.findViewById(R.id.snackbar_text)
|
||||
snackbarText.setTextSize(TypedValue.COMPLEX_UNIT_SP, resources.getDimension(R.dimen.small_text))
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for showing the CAN field to the user and can be used to refresh the field as well.
|
||||
*/
|
||||
private fun showCanField() {
|
||||
if (viewModel.userCan.length == 6) {
|
||||
binding.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
|
||||
binding.canMenuAction.text = getString(R.string.can_delete)
|
||||
} else {
|
||||
binding.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
|
||||
binding.canMenuAction.text = getString(R.string.add_can_text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that allows the user to delete saved CAN from the device and also to save new a CAN if
|
||||
* currently there is no CAN saved.
|
||||
*/
|
||||
private fun canAction() {
|
||||
if (viewModel.userCan.length == 6) {
|
||||
viewModel.deleteCan(requireContext())
|
||||
showCanField()
|
||||
showSnackbar(getString(R.string.can_deleted))
|
||||
} else {
|
||||
(activity as MainActivity).inMenu = false
|
||||
val action = SettingsFragmentDirections.actionSettingsFragmentToCanFragment(saving = true)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for showing the PIN 1 field to the user and can be used to refresh the field as well.
|
||||
* The PIN 1 is hidden by default and when it is hidden it is always shown as **** despite the
|
||||
* length of the PIN 1. Can be made visible with toggle button.
|
||||
*/
|
||||
private fun showPinField() {
|
||||
if (viewModel.userPin.length in 4..12) {
|
||||
binding.pinMenuShow.visibility = Button.VISIBLE
|
||||
if (showPin)
|
||||
binding.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
|
||||
else
|
||||
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
|
||||
binding.pinMenuAction.text = getString(R.string.pin1_delete)
|
||||
} else {
|
||||
binding.pinMenuShow.visibility = Button.GONE
|
||||
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
|
||||
binding.pinMenuAction.text = getString(R.string.pin1_add)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that allows the user to delete saved PIN 1 from the device and also to save a new PIN 1 if
|
||||
* currently there is no PIN 1 saved.
|
||||
*/
|
||||
private fun pinAction() {
|
||||
if (viewModel.userPin.length in 4..12) {
|
||||
viewModel.deletePin(requireContext())
|
||||
showPinField()
|
||||
showSnackbar(getString(R.string.pin_deleted))
|
||||
} else {
|
||||
(activity as MainActivity).inMenu = false
|
||||
val action = SettingsFragmentDirections.actionSettingsFragmentToPinFragment(saving = true)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the PIN 1 or makes it visible.
|
||||
*/
|
||||
private fun togglePin() {
|
||||
showPin = !showPin
|
||||
togglePinButton()
|
||||
showPinField()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the text on the button that controls the visiblity of the PIN 1.
|
||||
*/
|
||||
private fun togglePinButton() {
|
||||
if (showPin) {
|
||||
binding.pinMenuShow.text = getString(R.string.hide)
|
||||
} else {
|
||||
binding.pinMenuShow.text = getString(R.string.show)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to home fragment.
|
||||
*/
|
||||
private fun backToHome() {
|
||||
(activity as MainActivity).inMenu = false
|
||||
findNavController().navigate(R.id.action_settingsFragment_to_homeFragment)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp.model
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Log.WARN
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ParametersViewModel: ViewModel() {
|
||||
|
||||
private var _challenge: String = ""
|
||||
val challenge get() = _challenge
|
||||
|
||||
private var _authUrl: String = ""
|
||||
val authUrl get() = _authUrl
|
||||
|
||||
private var _token: String = ""
|
||||
val token get() = _token
|
||||
|
||||
private var _origin: String = ""
|
||||
val origin get() = _origin
|
||||
|
||||
private var _headers: Map<String, String> = HashMap<String, String>()
|
||||
val headers get() =_headers
|
||||
|
||||
fun setChallenge(newChallenge: String) {
|
||||
_challenge = newChallenge
|
||||
}
|
||||
|
||||
fun setAuthUrl(newAuthUrl: String) {
|
||||
_authUrl = newAuthUrl
|
||||
}
|
||||
|
||||
fun setToken(newToken: String) {
|
||||
_token = newToken
|
||||
}
|
||||
|
||||
fun setOrigin(newOrigin: String) {
|
||||
_origin = newOrigin
|
||||
}
|
||||
|
||||
fun setHeaders(newHeaders: Map<String, String>) {
|
||||
Log.i("HEADERS", newHeaders.toList().toString())
|
||||
_headers = newHeaders
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
|
||||
class SmartCardViewModel: ViewModel() {
|
||||
|
||||
private var _userPin: String = ""
|
||||
val userPin get() = _userPin
|
||||
|
||||
private var _userPin2: String = ""
|
||||
val userPin2 get() = _userPin2
|
||||
|
||||
private var _userCan: String = ""
|
||||
val userCan get() = _userCan
|
||||
|
||||
private var _userFirstName: String = ""
|
||||
val userFirstName get() = _userFirstName
|
||||
|
||||
private var _userLastName: String = ""
|
||||
val userLastName get() = _userLastName
|
||||
|
||||
private var _userIdentificationNumber: String = ""
|
||||
val userIdentificationNumber get() = _userIdentificationNumber
|
||||
|
||||
private var _gender: String = ""
|
||||
val gender get() = _gender
|
||||
|
||||
private var _expiration: String = ""
|
||||
val expiration get() = _expiration
|
||||
|
||||
private var _citizenship: String = ""
|
||||
val citizenship get() = _citizenship
|
||||
|
||||
fun clearUserInfo() {
|
||||
_userPin = ""
|
||||
_userPin2 = ""
|
||||
_userCan = ""
|
||||
_userFirstName = ""
|
||||
_userLastName = ""
|
||||
_userIdentificationNumber = ""
|
||||
_expiration = ""
|
||||
_citizenship = ""
|
||||
_gender = ""
|
||||
}
|
||||
|
||||
fun setUserPin(newUserPin: String) {
|
||||
_userPin = newUserPin
|
||||
}
|
||||
|
||||
fun setUserPin2(newUserPin2: String) {
|
||||
_userPin2 = newUserPin2
|
||||
}
|
||||
|
||||
fun setUserCan(newUserCan: String) {
|
||||
_userCan = newUserCan
|
||||
}
|
||||
|
||||
fun setUserFirstName(newUserFirstName: String) {
|
||||
_userFirstName = newUserFirstName
|
||||
}
|
||||
|
||||
fun setUserLastName(newUserLastName: String) {
|
||||
_userLastName = newUserLastName
|
||||
}
|
||||
|
||||
fun setUserIdentificationNumber(newUserIdentificationNumber: String) {
|
||||
_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 = ""
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM7.64,15H6.49v-4.5l-0.9,0.66l-0.58,-0.89L6.77,9h0.87V15zM13.5,15H9.61v-1.02c1.07,-1.07 1.77,-1.77 2.13,-2.15c0.4,-0.42 0.54,-0.69 0.54,-1.06c0,-0.4 -0.31,-0.72 -0.81,-0.72c-0.52,0 -0.8,0.39 -0.9,0.72l-1.01,-0.42c0.01,-0.02 0.18,-0.76 1,-1.15c0.69,-0.33 1.48,-0.2 1.95,0.03c0.86,0.44 0.91,1.24 0.91,1.48c0,0.64 -0.31,1.26 -0.92,1.86c-0.25,0.25 -0.72,0.71 -1.4,1.39l0.03,0.05h2.37V15zM18.75,14.15C18.67,14.28 18.19,15 16.99,15c-0.04,0 -1.6,0.08 -2.05,-1.51l1.03,-0.41c0.03,0.1 0.19,0.86 1.02,0.86c0.41,0 0.89,-0.28 0.89,-0.77c0,-0.55 -0.48,-0.79 -1.04,-0.79h-0.5v-1h0.46c0.33,0 0.88,-0.14 0.88,-0.72c0,-0.39 -0.31,-0.65 -0.75,-0.65c-0.5,0 -0.74,0.32 -0.85,0.64l-0.99,-0.41C15.2,9.9 15.68,9 16.94,9c1.09,0 1.54,0.64 1.62,0.75c0.33,0.5 0.28,1.16 0.02,1.57c-0.15,0.22 -0.32,0.38 -0.52,0.48v0.07c0.28,0.11 0.51,0.28 0.68,0.52C19.11,12.91 19.07,13.66 18.75,14.15z"/>
|
||||
</vector>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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="#001970"
|
||||
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>
|
|
@ -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>
|
|
@ -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="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z"/>
|
||||
</vector>
|
|
@ -0,0 +1,33 @@
|
|||
<?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"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
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>
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/padding"
|
||||
tools:context=".AuthFragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_small">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/auth_fragment_instruction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin"
|
||||
android:gravity="left"
|
||||
android:text="@string/auth_instruction_text"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nfc_logo"
|
||||
android:layout_width="@dimen/logo_big"
|
||||
android:layout_height="@dimen/logo_big"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/margin"
|
||||
android:padding="@dimen/margin_huge"
|
||||
android:src="@drawable/nfc_logo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_counter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintTop_toBottomOf="@id/auth_fragment_instruction"
|
||||
tools:text="@string/time_left" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- later there will be no button, but the app will continue automatically -->
|
||||
<Button
|
||||
android:id="@+id/next_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:text="@string/continue_button"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:layout_marginStart="@dimen/padding_tiny"
|
||||
android:text="@string/cancel_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/padding"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_text"
|
||||
android:text="@string/can_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/headline_text"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:fontFamily="sans-serif"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/canTextField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:hint="@string/can_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/can_helper_text"
|
||||
app:helperTextTextAppearance="@style/helper"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="6"
|
||||
app:counterTextAppearance="@style/helper"
|
||||
app:counterOverflowTextAppearance="@style/helper"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:fontFamily="sans-serif"
|
||||
android:inputType="number"
|
||||
android:singleLine="true"
|
||||
/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
android:text="@string/cancel_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:fontFamily="sans-serif"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/canTextField" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,128 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/padding"
|
||||
tools:context=".HomeFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/saved_states"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintStart_toStartOf="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="@dimen/margin_big"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/can_status_logo"
|
||||
android:layout_marginStart="@dimen/margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/can_status_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:padding="@dimen/margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/pin_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pin_status_logo"
|
||||
android:layout_marginStart="@dimen/margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin_status_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:padding="@dimen/margin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/id_card_detection"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/saved_states"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detection_action_text"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:text="@string/action_detect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/home_action_button"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:text="@string/try_again_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginStart="@dimen/margin_huge"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/id_card_detection"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/home_help_button"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:text="@string/help_text"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginStart="@dimen/margin_huge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/home_action_button"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/padding"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/pin_view"
|
||||
android:textSize="@dimen/headline_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/pinTextField"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:hint="@string/hint_pin"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="12"
|
||||
app:counterOverflowTextAppearance="@style/helper"
|
||||
app:counterTextAppearance="@style/helper"
|
||||
app:endIconMode="password_toggle"
|
||||
app:helperText="@string/pin_helper_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperTextTextAppearance="@style/helper"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif"
|
||||
android:inputType="numberPassword"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/save_pin_question"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:paddingTop="@dimen/padding"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/save_pin"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pinTextField" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/save_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/save_pin_question">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/save_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:checked="true"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/save_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/pin_save_on"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_continue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/continue_button"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/save_layout" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_big"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/cancel_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_continue" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="24dp"
|
||||
tools:context=".Pin2Fragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin2_fragment_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="6dp"
|
||||
android:text="@string/pin2_fragment" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/pin2_edit_text"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="6dp"
|
||||
android:hint="@string/enter_pin2"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="12"
|
||||
app:endIconMode="password_toggle"
|
||||
app:errorEnabled="true"
|
||||
app:helperText="@string/example_pin2"
|
||||
app:helperTextEnabled="true"
|
||||
app:startIconDrawable="@drawable/can_logo">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberPassword"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/next_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/continue_button"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/cancel_text"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/next_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/padding"
|
||||
tools:context=".ResultFragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/can_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_text"
|
||||
android:text="@string/result_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:padding="@dimen/padding_small"
|
||||
android:layout_marginVertical="@dimen/margin_small"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_info_text"
|
||||
android:text="@string/result_info"
|
||||
android:padding="@dimen/padding_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:layout_marginVertical="@dimen/margin_small"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/padding"
|
||||
tools:context=".menu.SettingsFragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/settings_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
<LinearLayout
|
||||
android:id="@+id/can_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_small">
|
||||
<TextView
|
||||
android:id="@+id/can_saved"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:padding="@dimen/margin_small"
|
||||
android:text="@string/saved_can" />
|
||||
<Button
|
||||
android:id="@+id/can_menu_action"
|
||||
android:layout_margin="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
<TextView
|
||||
android:id="@+id/pin_saved"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:padding="@dimen/margin_small"
|
||||
android:text="@string/saved_pin"/>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<Button
|
||||
android:id="@+id/pin_menu_show"
|
||||
android:layout_marginHorizontal="@dimen/margin"
|
||||
android:layout_marginVertical="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"/>
|
||||
<Button
|
||||
android:id="@+id/pin_menu_action"
|
||||
android:layout_marginHorizontal="@dimen/margin"
|
||||
android:layout_marginVertical="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/return_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/return_text"
|
||||
android:layout_marginVertical="@dimen/margin"
|
||||
android:layout_marginStart="@dimen/padding"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintTop_toBottomOf="@id/settings_card"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,127 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/padding"
|
||||
tools:context=".UserFragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="@color/stroke_color"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_tiny">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/user_name_label"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/user_name"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/identification_number_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:text="@string/identification_number_label"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/identification_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gender_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:text="@string/gender_label"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gender"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/expiration_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:text="@string/expiration_label"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/expiration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/citizenship_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:text="@string/citizenship_label"
|
||||
android:textSize="@dimen/regular_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/citizenship"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textSize="@dimen/regular_text"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/clear_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_big"
|
||||
android:text="@string/return_text"
|
||||
android:textSize="@dimen/regular_text"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -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>
|
|
@ -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_check_logo" />
|
||||
</adaptive-icon>
|
|
@ -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_check_logo" />
|
||||
</adaptive-icon>
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 982 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
|
@ -0,0 +1,158 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation 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:id="@+id/nav_graph"
|
||||
app:startDestination="@id/homeFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/homeFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.HomeFragment"
|
||||
android:label="fragment_home"
|
||||
tools:layout="@layout/fragment_home">
|
||||
<action
|
||||
android:id="@+id/action_homeFragment_to_settingsFragment"
|
||||
app:destination="@id/settingsFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<action
|
||||
android:id="@+id/action_homeFragment_to_canFragment"
|
||||
app:destination="@id/canFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<action
|
||||
android:id="@+id/action_homeFragment_to_userFragment"
|
||||
app:destination="@id/userFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/pinFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.PinFragment"
|
||||
android:label="fragment_pin"
|
||||
tools:layout="@layout/fragment_pin">
|
||||
<action
|
||||
android:id="@+id/action_pinFragment_to_homeFragment"
|
||||
app:destination="@id/homeFragment"
|
||||
app:popUpTo="@id/homeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
<action
|
||||
android:id="@+id/action_pinFragment_to_settingsFragment"
|
||||
app:destination="@id/settingsFragment"
|
||||
app:popUpTo="@id/settingsFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
<argument
|
||||
android:name="saving"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<action
|
||||
android:id="@+id/action_pinFragment_to_authFragment"
|
||||
app:destination="@id/authFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<argument
|
||||
android:name="auth"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
<argument
|
||||
android:name="mobile"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/canFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.CanFragment"
|
||||
android:label="fragment_can"
|
||||
tools:layout="@layout/fragment_can">
|
||||
<action
|
||||
android:id="@+id/action_canFragment_to_homeFragment"
|
||||
app:destination="@id/homeFragment"
|
||||
app:popUpTo="@id/homeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
<action
|
||||
android:id="@+id/action_canFragment_to_settingsFragment"
|
||||
app:destination="@id/settingsFragment"
|
||||
app:popUpTo="@id/settingsFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
<argument
|
||||
android:name="saving"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<action
|
||||
android:id="@+id/action_canFragment_to_pinFragment"
|
||||
app:destination="@id/pinFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<argument
|
||||
android:name="auth"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
<argument
|
||||
android:name="mobile"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
<argument
|
||||
android:name="fromhome"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/authFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.AuthFragment"
|
||||
android:label="fragment_auth"
|
||||
tools:layout="@layout/fragment_auth">
|
||||
<action
|
||||
android:id="@+id/action_authFragment_to_userFragment"
|
||||
app:destination="@id/userFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<action
|
||||
android:id="@+id/action_authFragment_to_homeFragment"
|
||||
app:destination="@id/homeFragment"
|
||||
app:popUpTo="@id/homeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
<action
|
||||
android:id="@+id/action_authFragment_to_resultFragment"
|
||||
app:destination="@id/resultFragment"
|
||||
app:popUpTo="@id/homeFragment" />
|
||||
<argument
|
||||
android:name="auth"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
<argument
|
||||
android:name="mobile"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/userFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.UserFragment"
|
||||
android:label="fragment_user"
|
||||
tools:layout="@layout/fragment_user">
|
||||
<action
|
||||
android:id="@+id/action_userFragment_to_homeFragment"
|
||||
app:destination="@id/homeFragment"
|
||||
app:popUpTo="@id/homeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settingsFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.menu.SettingsFragment"
|
||||
android:label="fragment_settings"
|
||||
tools:layout="@layout/fragment_settings">
|
||||
<action
|
||||
android:id="@+id/action_settingsFragment_to_canFragment"
|
||||
app:destination="@id/canFragment" />
|
||||
<action
|
||||
android:id="@+id/action_settingsFragment_to_pinFragment"
|
||||
app:destination="@id/pinFragment" />
|
||||
<action
|
||||
android:id="@+id/action_settingsFragment_to_homeFragment"
|
||||
app:destination="@id/homeFragment"
|
||||
app:popUpTo="@id/homeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/resultFragment"
|
||||
android:name="com.tarkvaraprojekt.mobileauthapp.ResultFragment"
|
||||
android:label="fragment_result"
|
||||
tools:layout="@layout/fragment_result" >
|
||||
<argument
|
||||
android:name="mobile"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
</navigation>
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Must translate to English, but should work now -->
|
||||
<string name="app_name">NFC authenticator</string>
|
||||
|
||||
<!-- BUTTONS -->
|
||||
<string name="cancel_text">CANCEL</string>
|
||||
<string name="return_text">BACK</string>
|
||||
<string name="add_can_text">ADD CAN</string>
|
||||
<string name="try_again_text">TRY AGAIN</string>
|
||||
<string name="continue_button">CONTINUE</string>
|
||||
|
||||
<!-- Card Detection related -->
|
||||
<string name="card_detected">Card detected. Hold it against the phone.</string>
|
||||
<string name="data_read">Data read. You can continue.</string>
|
||||
<string name="wrong_can_text">Wrong CAN</string>
|
||||
<string name="action_detect">Put the ID card against the phone to detect it</string>
|
||||
<string name="action_detect_unavailable">CAN must be added before ID card can be detected</string>
|
||||
<string name="nfc_not_available">NFC is not turned on or is not supported by the phone</string>
|
||||
<string name="nfc_reading_error">The provided CAN does not match the ID card</string>
|
||||
<string name="id_card_removed_early">ID card was removed too early</string>
|
||||
<string name="wrong_pin">Wrong PIN 1. Tries on the card left %s</string>
|
||||
|
||||
<!-- string resources for HomeFragment -->
|
||||
<string name="pin_status_saved">PIN 1 saved</string>
|
||||
<string name="pin_status_negative">PIN 1 not saved</string>
|
||||
<string name="can_status_saved">CAN saved</string>
|
||||
<string name="can_status_negative">CAN not saved</string>
|
||||
<string name="help_text">HELP</string>
|
||||
<string name="can_question">What is CAN?</string>
|
||||
<string name="can_explanation">CAN is a 6 digit code that is needed to communicate with an ID card. It can be found on the ID card under the card holder\'s picture with a title KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
|
||||
<string name="problem_parameters">Problem with parameters</string>
|
||||
<string name="problem_challenge">Challenge is missing</string>
|
||||
<string name="problem_authurl">AuthUrl is missing</string>
|
||||
<string name="problem_originurl">OriginUrl is missing</string>
|
||||
<string name="problem_other">Unspecified problem with parameters</string>
|
||||
<!-- string resources for PinFragment -->
|
||||
<string name="pin_view">Please enter PIN 1</string>
|
||||
<string name="hint_pin">PIN 1</string>
|
||||
<string name="pin_helper_text">PIN 1 must be 4–12 digits long</string>
|
||||
<string name="save_pin">Save PIN 1</string>
|
||||
<string name="pin_save_on">On</string>
|
||||
<string name="pin_save_off">Off</string>
|
||||
|
||||
<!-- string resources for Pin2Fragment -->
|
||||
<string name="pin2_fragment">Please enter PIN 2</string>
|
||||
<string name="enter_pin2">PIN 2</string>
|
||||
<string name="example_pin2">Example. 123456</string>
|
||||
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
|
||||
|
||||
<!-- string resources for CanFragment -->
|
||||
<string name="can_view">Please enter CAN</string>
|
||||
<string name="can_text">CAN</string>
|
||||
<string name="can_helper_text">CAN must be 6 digits long</string>
|
||||
|
||||
<!-- string resources for AuthFragment layout -->
|
||||
<string name="auth_instruction_text">Put the ID card against the phone</string>
|
||||
<string name="time_left">Time left %d sek</string>
|
||||
<string name="no_time">No time left</string>
|
||||
|
||||
<!-- string resources for UserFragment layout -->
|
||||
<string name="user_name_label">NAME</string>
|
||||
<string name="user_name">%1$s %2$s</string>
|
||||
<string name="identification_number_label">IDENTIFICATION NUMBER</string>
|
||||
<string name="expiration_label">DATE OF EXPIRY</string>
|
||||
<string name="citizenship_label">CITIZENSHIP</string>
|
||||
<string name="gender_label">SEX</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">Checking the created token</string>
|
||||
<string name="result_info">The app will close automatically</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Settings</string>
|
||||
<string name="saved_can">CAN: %s</string>
|
||||
<string name="can_delete">Delete CAN</string>
|
||||
<string name="saved_pin">PIN1: %s</string>
|
||||
<string name="pin1_add">Add PIN1</string>
|
||||
<string name="pin1_delete">Delete PIN1</string>
|
||||
<string name="missing">not saved</string>
|
||||
<string name="show">SHOW</string>
|
||||
<string name="hide">HIDE</string>
|
||||
<string name="hidden_pin">****</string>
|
||||
<string name="menu_unavailable_message">Settings are currently unavailable</string>
|
||||
<string name="can_deleted">CAN deleted</string>
|
||||
<string name="pin_deleted">PIN 1 deleted</string>
|
||||
</resources>
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">NFC autentija</string>
|
||||
|
||||
<!-- Buttons -->
|
||||
<string name="cancel_text">KATKESTA</string>
|
||||
<string name="return_text">TAGASI</string>
|
||||
<string name="add_can_text">LISA CAN</string>
|
||||
<string name="try_again_text">ÜRITA UUESTI</string>
|
||||
<string name="continue_button">JÄTKA</string>
|
||||
|
||||
<!-- Card Detection related -->
|
||||
<string name="card_detected">Kaart tuvastatud. Hoia kaarti vastu telefoni.</string>
|
||||
<string name="data_read">Andmed loetud, võid jätkata.</string>
|
||||
<string name="wrong_can_text">Vale CAN</string>
|
||||
<string name="action_detect">ID kaardi tuvastamiseks pane kaart vastu telefoni</string>
|
||||
<string name="action_detect_unavailable">ID kaardi tuvastamiseks peab olema CAN lisatud</string>
|
||||
<string name="nfc_not_available">NFC ei ole sisse lülitatud või puudub telefonil NFC võimekus</string>
|
||||
<string name="nfc_reading_error">Sisestatud CAN ei ole vastavuses ID kaardiga</string>
|
||||
<string name="id_card_removed_early">ID kaart eemaldati liiga vara</string>
|
||||
<string name="wrong_pin">Vale PIN 1. ID kaardil PIN 1 sisetamise kordi alles: %s</string>
|
||||
|
||||
<!-- string resources for HomeFragment -->
|
||||
<string name="pin_status_saved">PIN 1 on salvestatud</string>
|
||||
<string name="pin_status_negative">PIN 1 ei ole salvestatud</string>
|
||||
<string name="can_status_saved">CAN on salvestatud</string>
|
||||
<string name="can_status_negative">CAN ei ole salvestatud</string>
|
||||
<string name="help_text">INFO</string>
|
||||
<string name="can_question">Mis on CAN?</string>
|
||||
<string name="can_explanation">CAN on 6 kohaline numbritest koosnev kood, mida on vaja ID kaardiga suhtlemiseks. CAN-i leiab ID kaardilt omaniku pildi alt pealkirjaga KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
|
||||
<string name="problem_parameters">Probleem parameetritega</string>
|
||||
<string name="problem_challenge">Puudub challenge parameeter</string>
|
||||
<string name="problem_authurl">Puudub AuthUrl parameeter</string>
|
||||
<string name="problem_originurl">Puudub OriginUrl parameeter</string>
|
||||
<string name="problem_other">Täpsustamata probleem parameetritega</string>
|
||||
<!-- string resources for PinFragment -->
|
||||
<string name="pin_view">Palun sisesta PIN 1</string>
|
||||
<string name="hint_pin">PIN 1</string>
|
||||
<string name="pin_helper_text">PIN 1 lubatud pikkus on 4..12</string>
|
||||
<string name="save_pin">Save PIN 1</string>
|
||||
<string name="pin_save_on">On</string>
|
||||
<string name="pin_save_off">Off</string>
|
||||
|
||||
<!-- string resources for Pin2Fragment -->
|
||||
<string name="pin2_fragment">Palun sisesta PIN 2</string>
|
||||
<string name="enter_pin2">PIN 2</string>
|
||||
<string name="example_pin2">Näide. 123456</string>
|
||||
<string name="length_pin2">PIN 2 lubatud pikkus on 5..12</string>
|
||||
|
||||
<!-- string resources for CanFragment -->
|
||||
<string name="can_view">Please enter CAN</string>
|
||||
<string name="can_text">CAN</string>
|
||||
<string name="can_helper_text">CAN must be 6 digits long</string>
|
||||
|
||||
<!-- string resources for AuthFragment layout -->
|
||||
<string name="auth_instruction_text">Pane ID kaart vastu telefoni</string>
|
||||
<string name="time_left">Aega on jäänud %d sek</string>
|
||||
<string name="no_time">Aeg on otsas</string>
|
||||
|
||||
<!-- string resources for UserFragment layout -->
|
||||
<string name="user_name_label">NIMI</string>
|
||||
<string name="user_name">%1$s %2$s</string>
|
||||
<string name="identification_number_label">ISIKUKOOD</string>
|
||||
<string name="expiration_label">KEHTIV KUNI</string>
|
||||
<string name="citizenship_label">KODAKONDSUS</string>
|
||||
<string name="gender_label">SUGU</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">Tulemust kontrollitakse</string>
|
||||
<string name="result_info">Rakendus sulgeb ennast ise</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Seaded</string>
|
||||
<string name="saved_can">CAN: %s</string>
|
||||
<string name="can_delete">Kustuta CAN</string>
|
||||
<string name="saved_pin">PIN1: %s</string>
|
||||
<string name="pin1_add">Lisa PIN1</string>
|
||||
<string name="pin1_delete">Kustuta PIN1</string>
|
||||
<string name="missing">puudub</string>
|
||||
<string name="show">NÄITA</string>
|
||||
<string name="hide">PEIDA</string>
|
||||
<string name="hidden_pin">****</string>
|
||||
<string name="menu_unavailable_message">Seaded pole hetkel saadaval</string>
|
||||
<string name="can_deleted">CAN kustatud</string>
|
||||
<string name="pin_deleted">PIN 1 kustatud</string>
|
||||
</resources>
|
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.MobileAuthApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/blue_200</item>
|
||||
<item name="colorPrimaryVariant">@color/blue_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/orange_200</item>
|
||||
<item name="colorSecondaryVariant">@color/orange_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>
|
|
@ -0,0 +1,16 @@
|
|||
<?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>
|
||||
|
||||
<color name="blue_200">#d1d9ff</color>
|
||||
<color name="blue_500">#002984</color>
|
||||
<color name="blue_700">#001970</color>
|
||||
<color name="orange_200">#ffab91</color>
|
||||
<color name="orange_700">#f57c00</color>
|
||||
</resources>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="margin_small">4dp</dimen>
|
||||
<dimen name="margin">8dp</dimen>
|
||||
<dimen name="margin_big">16dp</dimen>
|
||||
<dimen name="margin_huge">32dp</dimen>
|
||||
<dimen name="padding_tiny">8dp</dimen>
|
||||
<dimen name="padding_small">16dp</dimen>
|
||||
<dimen name="padding">24dp</dimen>
|
||||
<dimen name="regular_text">24sp</dimen>
|
||||
<dimen name="headline_text">32sp</dimen>
|
||||
<dimen name="helper_text">16sp</dimen>
|
||||
<dimen name="small_text">8sp</dimen>
|
||||
<dimen name="logo_big">128dp</dimen>
|
||||
</resources>
|
|
@ -0,0 +1,85 @@
|
|||
<resources>
|
||||
<string name="app_name">NFC authenticator</string>
|
||||
|
||||
<!-- BUTTONS -->
|
||||
<string name="cancel_text">CANCEL</string>
|
||||
<string name="return_text">BACK</string>
|
||||
<string name="add_can_text">ADD CAN</string>
|
||||
<string name="try_again_text">TRY AGAIN</string>
|
||||
<string name="continue_button">CONTINUE</string>
|
||||
|
||||
<!-- Card Detection related -->
|
||||
<string name="card_detected">Card detected. Hold it against the phone.</string>
|
||||
<string name="data_read">Data read. You can continue.</string>
|
||||
<string name="wrong_can_text">Wrong CAN</string>
|
||||
<string name="action_detect">Put the ID card against the phone to detect it</string>
|
||||
<string name="action_detect_unavailable">CAN must be added before ID card can be detected</string>
|
||||
<string name="nfc_not_available">NFC is not turned on or is not supported by the phone</string>
|
||||
<string name="nfc_reading_error">The provided CAN does not match the ID card</string>
|
||||
<string name="id_card_removed_early">ID card was removed too early</string>
|
||||
<string name="wrong_pin">Wrong PIN 1. Tries on the card left %s</string>
|
||||
|
||||
<!-- string resources for HomeFragment -->
|
||||
<string name="pin_status_saved">PIN 1 saved</string>
|
||||
<string name="pin_status_negative">PIN 1 not saved</string>
|
||||
<string name="can_status_saved">CAN saved</string>
|
||||
<string name="can_status_negative">CAN not saved</string>
|
||||
<string name="help_text">HELP</string>
|
||||
<string name="can_question">What is CAN?</string>
|
||||
<string name="can_explanation">CAN is a 6 digit code that is needed to communicate with an ID card. It can be found on the ID card under the card holder\'s picture with a title KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
|
||||
<string name="problem_parameters">Problem with parameters</string>
|
||||
<string name="problem_challenge">Challenge is missing</string>
|
||||
<string name="problem_authurl">AuthUrl is missing</string>
|
||||
<string name="problem_originurl">OriginUrl is missing</string>
|
||||
<string name="problem_other">Unspecified problem with parameters</string>
|
||||
<!-- string resources for PinFragment -->
|
||||
<string name="pin_view">Please enter PIN 1</string>
|
||||
<string name="hint_pin">PIN 1</string>
|
||||
<string name="pin_helper_text">PIN 1 must be 4–12 digits long</string>
|
||||
<string name="save_pin">Save PIN 1</string>
|
||||
<string name="pin_save_on">On</string>
|
||||
<string name="pin_save_off">Off</string>
|
||||
|
||||
<!-- string resources for Pin2Fragment -->
|
||||
<string name="pin2_fragment">Please enter PIN 2</string>
|
||||
<string name="enter_pin2">PIN 2</string>
|
||||
<string name="example_pin2">Example. 123456</string>
|
||||
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
|
||||
|
||||
<!-- string resources for CanFragment -->
|
||||
<string name="can_view">Please enter CAN</string>
|
||||
<string name="can_text">CAN</string>
|
||||
<string name="can_helper_text">CAN must be 6 digits long</string>
|
||||
|
||||
<!-- string resources for AuthFragment layout -->
|
||||
<string name="auth_instruction_text">Put the ID card against the phone</string>
|
||||
<string name="time_left">Time left %d sek</string>
|
||||
<string name="no_time">No time left</string>
|
||||
|
||||
<!-- string resources for UserFragment layout -->
|
||||
<string name="user_name_label">NAME</string>
|
||||
<string name="user_name">%1$s %2$s</string>
|
||||
<string name="identification_number_label">IDENTIFICATION NUMBER</string>
|
||||
<string name="expiration_label">DATE OF EXPIRY</string>
|
||||
<string name="citizenship_label">CITIZENSHIP</string>
|
||||
<string name="gender_label">SEX</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">Controlling the created token</string>
|
||||
<string name="result_info">The app will close automatically</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Settings</string>
|
||||
<string name="saved_can">CAN: %s</string>
|
||||
<string name="can_delete">Delete CAN</string>
|
||||
<string name="saved_pin">PIN1: %s</string>
|
||||
<string name="pin1_add">Add PIN 1</string>
|
||||
<string name="pin1_delete">Delete PIN 1</string>
|
||||
<string name="missing">not saved</string>
|
||||
<string name="show">SHOW</string>
|
||||
<string name="hide">HIDE</string>
|
||||
<string name="hidden_pin">****</string>
|
||||
<string name="menu_unavailable_message">Settings are currently unavailable</string>
|
||||
<string name="can_deleted">CAN deleted</string>
|
||||
<string name="pin_deleted">PIN 1 deleted</string>
|
||||
</resources>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="helper">
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textSize">@dimen/helper_text</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,16 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.MobileAuthApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/blue_500</item>
|
||||
<item name="colorPrimaryVariant">@color/blue_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/orange_200</item>
|
||||
<item name="colorSecondaryVariant">@color/orange_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>
|
|
@ -0,0 +1,17 @@
|
|||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
ext {
|
||||
nav_version = "2.3.1"
|
||||
kotlin_version = "1.4.30"
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
#Fri Sep 24 20:04:57 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
|
|
@ -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" "$@"
|
|
@ -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
|
|
@ -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 = "Mobile Auth App"
|
||||
include ':app'
|
22
README.md
|
@ -2,12 +2,24 @@
|
|||
|
||||
This is a proof-of-concept project for creating an Android app for authenticating yourself using an NFC-enabled Estonian ID card. This project will be created for the University of Tartu course "Software project".
|
||||
|
||||
[Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision)
|
||||
### Requirements to use the application
|
||||
* The smartphone's operating system must be Android 8.0 or newer
|
||||
* The smartphone must support NFC technology and it must be enabled
|
||||
* The user must have an Estonian ID card with NFC capability (issued since 2018)
|
||||
|
||||
[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)
|
||||
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/
|
||||
|
||||
**NB! Before using the application make sure that the NFC is enabled on the phone, otherwise information can not be read from the ID card.**
|
||||
|
||||
### Testing the application
|
||||
The project comes with a test mobile application and a test web application that can be used to try the MobileAuthApp authentication feature even if you don't have any web applications or mobile applications that require user authentication. Both projects come with a README file that help with a setup.
|
||||
The mobile authentication application, when launched by the user not a website or some other application, can also read card holder's information, which can be used to verify whether the application reads the information from the ID card correctly.
|
||||
|
||||
### See the [Wiki](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki) for pages relevant for the "Software project" subject
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
# TestMobileApp overview
|
||||
### The purpose
|
||||
The TestMobileApp was created in order to demonstrate how a different application on the Android smartphone could use the MobileAuthApp for user authentication purposes.
|
||||
### Installing the application
|
||||
The application installation process is the same as with the MobileAuthApp. Check the guide in the project's [main readme file](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC#installing-the-application-on-the-phone).
|
||||
### Using the application
|
||||
In order to use this application a backend server must be running that can issue challenges and verify the token created by the MobileAuthApp.
|
||||
Use demoBackend application that is included in the project. Follow the demoBackend setup guide and once you have a backend running take the https address of the backend
|
||||
and add it in the TestMobileApp's MainActivty.kt file as the new value for the constant variable BASE_URL (this is easly noticeable in the class as it is pointed out with a comment).
|
||||
Now the app can be used.
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,47 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.testmobileapp"
|
||||
minSdk 26
|
||||
targetSdk 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.koushikdutta.ion:ion:3.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.testmobileapp">
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TestMobileApp">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,94 @@
|
|||
package com.example.testmobileapp
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.example.testmobileapp.databinding.ActivityMainBinding
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Base url where the requests should be made. Add yours here. It must use https.
|
||||
*/
|
||||
private const val BASE_URL = "https://a0fe-2001-7d0-88ab-b880-7571-cba0-5db2-11b7.ngrok.io"
|
||||
private const val AUTH_URL = "$BASE_URL/auth/login"
|
||||
private const val CHALLENGE_URL = "$BASE_URL/auth/challenge"
|
||||
|
||||
/**
|
||||
* Test mobile app to demonstrate how other applications could potentially use MobileAuthApp.
|
||||
* Single purpose app that launches the MobileAuthApp and gets the response back (JWT).
|
||||
* Only for demo purposes.
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var authLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
|
||||
if (response.resultCode == Activity.RESULT_OK) {
|
||||
binding.loginTextView.text = getString(R.string.auth_success)
|
||||
// Logs are used to show what information can be retrieved from the mobileauthapp.
|
||||
Log.i("getResult", response.data?.getStringExtra("idCode").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("name").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("authority").toString())
|
||||
var user = ""
|
||||
try {
|
||||
user = response.data?.getStringExtra("name").toString()
|
||||
} catch (e: Exception) {
|
||||
Log.i("getResult", "unable to retrieve name")
|
||||
}
|
||||
showResult(user)
|
||||
}
|
||||
if (response.resultCode == Activity.RESULT_CANCELED) {
|
||||
binding.loginTextView.text = getString(R.string.auth_failure)
|
||||
}
|
||||
}
|
||||
|
||||
showLogin()
|
||||
|
||||
binding.loginOptionNfcButton.setOnClickListener {
|
||||
launchAuth()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates an intent to launch the MobileAuthApp
|
||||
*/
|
||||
private fun launchAuth() {
|
||||
val launchIntent = Intent()
|
||||
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
|
||||
launchIntent.putExtra("action", "auth")
|
||||
launchIntent.putExtra("challenge", CHALLENGE_URL)
|
||||
launchIntent.putExtra("originUrl", BASE_URL)
|
||||
launchIntent.putExtra("authUrl", AUTH_URL)
|
||||
launchIntent.putExtra("headers","${(0..100000).random()}")
|
||||
launchIntent.putExtra("mobile", true)
|
||||
authLauncher.launch(launchIntent)
|
||||
}
|
||||
|
||||
private fun showLogin() {
|
||||
binding.loginOptions.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showResult(user: String) {
|
||||
binding.loginOptions.visibility = View.GONE
|
||||
binding.resultLayout.visibility = View.VISIBLE
|
||||
binding.resultObject.text = getString(R.string.hello, user)
|
||||
binding.buttonForget.setOnClickListener {
|
||||
binding.loginTextView.text = getString(R.string.login_text)
|
||||
binding.resultObject.text = ""
|
||||
binding.resultLayout.visibility = View.GONE
|
||||
binding.loginOptions.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="24dp"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/login_text_view"
|
||||
android:text="@string/login_text"
|
||||
android:textSize="20sp"
|
||||
android:padding="12dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/login_options"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/login_text_view"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_method_text_view"
|
||||
android:text="@string/choose_login_method"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginVertical="6dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/login_option_nfc_button"
|
||||
android:text="@string/method_nfc"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginVertical="6dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toBottomOf="@id/login_text_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_object"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="6dp"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_forget"
|
||||
android:text="@string/forget_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 982 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 7.6 KiB |
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">TestMobileApp</string>
|
||||
<string name="login_text">Login</string>
|
||||
<string name="choose_login_method">Choose login method</string>
|
||||
<string name="method_nfc">NFC auth</string>
|
||||
<string name="auth_success">Logged in</string>
|
||||
<string name="auth_failure">Response failed</string>
|
||||
<string name="forget_button">Forget</string>
|
||||
<string name="hello">Hello, %s!</string>
|
||||
</resources>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">TestMobileApp</string>
|
||||
<string name="login_text">Logi sisse</string>
|
||||
<string name="choose_login_method">Vali sobiv meetod</string>
|
||||
<string name="method_nfc">NFC auth</string>
|
||||
<string name="auth_success">Sisse logimine õnnestus</string>
|
||||
<string name="auth_failure">Vastust ei õnnestunud kätte saada</string>
|
||||
<string name="forget_button">Unusta</string>
|
||||
<string name="hello">Tere, %s!</string>
|
||||
</resources>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,10 @@
|
|||
<resources>
|
||||
<string name="app_name">TestMobileApp</string>
|
||||
<string name="login_text">Login</string>
|
||||
<string name="choose_login_method">Choose login method</string>
|
||||
<string name="method_nfc">NFC auth</string>
|
||||
<string name="auth_success">Logged in</string>
|
||||
<string name="auth_failure">Response failed</string>
|
||||
<string name="forget_button">Forget</string>
|
||||
<string name="hello">Hello, %s!</string>
|
||||
</resources>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|