Compare commits

...

122 Commits

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

33
.github/workflows/main.yml vendored Normal file
View File

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

72
.gitignore vendored Normal file
View File

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

9
MobileAuthApp/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/captures
.externalNativeBuild
.cxx
local.properties

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

@ -0,0 +1 @@
/build

View File

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

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

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

View File

@ -0,0 +1,24 @@
package com.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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 { _, _ ->
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = ""
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 412 digits long</string>
<string name="save_pin">Save PIN 1</string>
<string name="pin_save_on">On</string>
<string name="pin_save_off">Off</string>
<!-- string resources for Pin2Fragment -->
<string name="pin2_fragment">Please enter PIN 2</string>
<string name="enter_pin2">PIN 2</string>
<string name="example_pin2">Example. 123456</string>
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
<!-- string resources for CanFragment -->
<string name="can_view">Please enter CAN</string>
<string name="can_text">CAN</string>
<string name="can_helper_text">CAN must be 6 digits long</string>
<!-- string resources for AuthFragment layout -->
<string name="auth_instruction_text">Put the ID card against the phone</string>
<string name="time_left">Time left %d sek</string>
<string name="no_time">No time left</string>
<!-- string resources for UserFragment layout -->
<string name="user_name_label">NAME</string>
<string name="user_name">%1$s %2$s</string>
<string name="identification_number_label">IDENTIFICATION NUMBER</string>
<string name="expiration_label">DATE OF EXPIRY</string>
<string name="citizenship_label">CITIZENSHIP</string>
<string name="gender_label">SEX</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">Controlling the created token</string>
<string name="result_info">The app will close automatically</string>
<!-- menu -->
<string name="menu_settings_title">Settings</string>
<string name="saved_can">CAN: %s</string>
<string name="can_delete">Delete CAN</string>
<string name="saved_pin">PIN1: %s</string>
<string name="pin1_add">Add PIN 1</string>
<string name="pin1_delete">Delete PIN 1</string>
<string name="missing">not saved</string>
<string name="show">SHOW</string>
<string name="hide">HIDE</string>
<string name="hidden_pin">****</string>
<string name="menu_unavailable_message">Settings are currently unavailable</string>
<string name="can_deleted">CAN deleted</string>
<string name="pin_deleted">PIN 1 deleted</string>
</resources>

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

185
MobileAuthApp/gradlew vendored Normal file
View File

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

89
MobileAuthApp/gradlew.bat vendored Normal file
View File

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

View File

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

View File

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

16
TestMobileApp/.gitignore vendored Normal file
View File

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

10
TestMobileApp/README.md Normal file
View File

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

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

@ -0,0 +1 @@
/build

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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