re-added passport-reader
6
.gitmodules
vendored
@@ -1,6 +0,0 @@
|
||||
[submodule "passport-reader"]
|
||||
path = passport-reader
|
||||
url = git@github.com:tananaev/passport-reader.git
|
||||
[submodule "zkrsa"]
|
||||
path = zkrsa
|
||||
url = git@github.com:dmpierre/zkrsa.git
|
||||
20
passport-reader/.github/workflows/android.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '11'
|
||||
- run: ./gradlew build
|
||||
23
passport-reader/.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Update Master List
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Update MasterList file
|
||||
run : wget "https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/ElekAusweise/CSCA/GermanMasterList.zip?__blob=publicationFile" -O- | zcat | openssl cms -inform DER -verify -noverify -out app/src/main/assets/masterList
|
||||
|
||||
- name: Commit new masterList
|
||||
run: |
|
||||
set +e
|
||||
git add .
|
||||
git config user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
||||
git config user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
||||
git commit -m "Update masterList"
|
||||
git push "https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY"
|
||||
7
passport-reader/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.gradle
|
||||
.idea
|
||||
.DS_Store
|
||||
local.properties
|
||||
google-services.json
|
||||
*.iml
|
||||
build
|
||||
7
passport-reader/PRIVACY.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Privacy Policy
|
||||
|
||||
We are not interested in collecting any personal information. We do not store or transmit your personal details, nor do we include any advertising or analytics software that talks to third parties.
|
||||
|
||||
# Contact
|
||||
|
||||
If you have any questions or concerns, please feel free to contact us via GitHub issues.
|
||||
36
passport-reader/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# e-Passport NFC Reader
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.tananaev.passportreader) [](https://f-droid.org/packages/com.tananaev.passportreader)
|
||||
|
||||
Android app that uses the NFC chip to communicate with an electronic passport.
|
||||
|
||||
## Contacts
|
||||
|
||||
Author - Anton Tananaev ([anton.tananaev@gmail.com](mailto:anton.tananaev@gmail.com))
|
||||
|
||||
## Dependencies
|
||||
|
||||
Note that the app includes following third party dependencies:
|
||||
|
||||
- JMRTD - [LGPL 3.0 License](https://www.gnu.org/licenses/lgpl-3.0.en.html)
|
||||
- SCUBA (Smart Card Utils) - [LGPL 3.0 License](https://www.gnu.org/licenses/lgpl-3.0.en.html)
|
||||
- Spongy Castle - MIT-based [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html)
|
||||
- JP2 for Android - [BSD 2-Clause License](https://opensource.org/licenses/BSD-2-Clause)
|
||||
- JNBIS - [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- Material DateTimepicker - [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
## License
|
||||
|
||||
Apache License, Version 2.0
|
||||
|
||||
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
|
||||
|
||||
http://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.
|
||||
69
passport-reader/app/build.gradle
Normal file
@@ -0,0 +1,69 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
ndkVersion '23.1.7779620'
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.tananaev.passportreader'
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 33
|
||||
versionCode 19
|
||||
versionName '3.0'
|
||||
multiDexEnabled = true
|
||||
}
|
||||
namespace 'com.tananaev.passportreader'
|
||||
|
||||
flavorDimensions 'default'
|
||||
productFlavors {
|
||||
regular {
|
||||
isDefault = true
|
||||
}
|
||||
google
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['META-INF/LICENSE', 'META-INF/NOTICE']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'com.wdullaer:materialdatetimepicker:3.5.2'
|
||||
implementation 'org.jmrtd:jmrtd:0.7.18'
|
||||
implementation 'net.sf.scuba:scuba-sc-android:0.0.18'
|
||||
implementation 'com.madgag.spongycastle:prov:1.54.0.0'
|
||||
implementation 'com.gemalto.jp2:jp2-android:1.0.3'
|
||||
implementation 'com.github.mhshams:jnbis:1.1.0'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.65' // do not update
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:31.0.0')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics'
|
||||
googleImplementation 'com.google.android.gms:play-services-ads:21.3.0'
|
||||
googleImplementation 'com.google.android.play:review-ktx:2.0.1'
|
||||
}
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains('Google')) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
task copyJson(type: Copy) {
|
||||
from '../../environment/firebase'
|
||||
into '.'
|
||||
include 'passport-reader.json'
|
||||
rename('passport-reader.json', 'google-services.json')
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preBuild.dependsOn copyJson
|
||||
}
|
||||
}
|
||||
24
passport-reader/app/src/google/AndroidManifest.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".GoogleActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="com.tananaev.passportreader.REQUEST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.tananaev.passportreader
|
||||
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.gms.ads.AdRequest
|
||||
import com.google.android.gms.ads.AdSize
|
||||
import com.google.android.gms.ads.AdView
|
||||
import com.google.android.gms.ads.MobileAds
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
|
||||
class GoogleActivity : MainActivity() {
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
MobileAds.initialize(this) {}
|
||||
|
||||
val adView = AdView(this).apply {
|
||||
setAdSize(AdSize.BANNER)
|
||||
adUnitId = "ca-app-pub-9061647223840223/5869276959"
|
||||
loadAd(AdRequest.Builder().build())
|
||||
}
|
||||
val params = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
val containerView: FrameLayout = findViewById(R.id.bottom_container)
|
||||
containerView.addView(adView, params)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
handleRating()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun handleRating() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (!preferences.getBoolean("ratingShown", false)) {
|
||||
val openTimes = preferences.getInt("openTimes", 0) + 1
|
||||
preferences.edit().putInt("openTimes", openTimes).apply()
|
||||
if (openTimes >= 5) {
|
||||
val reviewManager = ReviewManagerFactory.create(this)
|
||||
reviewManager.requestReviewFlow().addOnCompleteListener { infoTask ->
|
||||
if (infoTask.isSuccessful) {
|
||||
val flow = reviewManager.launchReviewFlow(this, infoTask.result)
|
||||
flow.addOnCompleteListener {
|
||||
preferences.edit().putBoolean("ratingShown", true).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
passport-reader/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:name=".MainApplication"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||
android:value="ca-app-pub-9061647223840223~3001602354"/>
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ResultActivity"
|
||||
android:screenOrientation="fullSensor" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
passport-reader/app/src/main/assets/masterList
Normal file
BIN
passport-reader/app/src/main/ic_launcher-web.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
package com.tananaev.passportreader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.gemalto.jp2.JP2Decoder
|
||||
import org.jnbis.WsqDecoder
|
||||
import java.io.InputStream
|
||||
|
||||
object ImageUtil {
|
||||
|
||||
fun decodeImage(context: Context?, mimeType: String, inputStream: InputStream?): Bitmap {
|
||||
return if (mimeType.equals("image/jp2", ignoreCase = true) || mimeType.equals(
|
||||
"image/jpeg2000",
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
JP2Decoder(inputStream).decode()
|
||||
} else if (mimeType.equals("image/x-wsq", ignoreCase = true)) {
|
||||
val wsqDecoder = WsqDecoder()
|
||||
val bitmap = wsqDecoder.decode(inputStream)
|
||||
val byteData = bitmap.pixels
|
||||
val intData = IntArray(byteData.size)
|
||||
for (j in byteData.indices) {
|
||||
intData[j] = 0xFF000000.toInt() or
|
||||
(byteData[j].toInt() and 0xFF shl 16) or
|
||||
(byteData[j].toInt() and 0xFF shl 8) or
|
||||
(byteData[j].toInt() and 0xFF)
|
||||
}
|
||||
Bitmap.createBitmap(
|
||||
intData,
|
||||
0,
|
||||
bitmap.width,
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
} else {
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||
|
||||
package com.tananaev.passportreader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tananaev.passportreader.ImageUtil.decodeImage
|
||||
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
|
||||
import net.sf.scuba.smartcards.CardService
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.bouncycastle.asn1.ASN1InputStream
|
||||
import org.bouncycastle.asn1.ASN1Primitive
|
||||
import org.bouncycastle.asn1.ASN1Sequence
|
||||
import org.bouncycastle.asn1.ASN1Set
|
||||
import org.bouncycastle.asn1.x509.Certificate
|
||||
import org.jmrtd.BACKey
|
||||
import org.jmrtd.BACKeySpec
|
||||
import org.jmrtd.PassportService
|
||||
import org.jmrtd.lds.CardAccessFile
|
||||
import org.jmrtd.lds.ChipAuthenticationPublicKeyInfo
|
||||
import org.jmrtd.lds.PACEInfo
|
||||
import org.jmrtd.lds.SODFile
|
||||
import org.jmrtd.lds.SecurityInfo
|
||||
import org.jmrtd.lds.icao.DG14File
|
||||
import org.jmrtd.lds.icao.DG1File
|
||||
import org.jmrtd.lds.icao.DG2File
|
||||
import org.jmrtd.lds.iso19794.FaceImageInfo
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.PKIXParameters
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.MGF1ParameterSpec
|
||||
import java.security.spec.PSSParameterSpec
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
abstract class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var passportNumberView: EditText
|
||||
private lateinit var expirationDateView: EditText
|
||||
private lateinit var birthDateView: EditText
|
||||
private var passportNumberFromIntent = false
|
||||
private var encodePhotoToBase64 = false
|
||||
private lateinit var mainLayout: View
|
||||
private lateinit var loadingLayout: View
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val dateOfBirth = intent.getStringExtra("dateOfBirth")
|
||||
val dateOfExpiry = intent.getStringExtra("dateOfExpiry")
|
||||
val passportNumber = intent.getStringExtra("passportNumber")
|
||||
encodePhotoToBase64 = intent.getBooleanExtra("photoAsBase64", false)
|
||||
if (dateOfBirth != null) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit().putString(KEY_BIRTH_DATE, dateOfBirth).apply()
|
||||
}
|
||||
if (dateOfExpiry != null) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit().putString(KEY_EXPIRATION_DATE, dateOfExpiry).apply()
|
||||
}
|
||||
if (passportNumber != null) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit().putString(KEY_PASSPORT_NUMBER, passportNumber).apply()
|
||||
passportNumberFromIntent = true
|
||||
}
|
||||
|
||||
passportNumberView = findViewById(R.id.input_passport_number)
|
||||
expirationDateView = findViewById(R.id.input_expiration_date)
|
||||
birthDateView = findViewById(R.id.input_date_of_birth)
|
||||
mainLayout = findViewById(R.id.main_layout)
|
||||
loadingLayout = findViewById(R.id.loading_layout)
|
||||
|
||||
passportNumberView.setText(preferences.getString(KEY_PASSPORT_NUMBER, null))
|
||||
expirationDateView.setText(preferences.getString(KEY_EXPIRATION_DATE, null))
|
||||
birthDateView.setText(preferences.getString(KEY_BIRTH_DATE, null))
|
||||
|
||||
passportNumberView.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this@MainActivity)
|
||||
.edit().putString(KEY_PASSPORT_NUMBER, s.toString()).apply()
|
||||
}
|
||||
})
|
||||
|
||||
expirationDateView.setOnClickListener {
|
||||
val c = loadDate(expirationDateView)
|
||||
val dialog = DatePickerDialog.newInstance(
|
||||
{ _, year, monthOfYear, dayOfMonth ->
|
||||
saveDate(
|
||||
expirationDateView,
|
||||
year,
|
||||
monthOfYear,
|
||||
dayOfMonth,
|
||||
KEY_EXPIRATION_DATE,
|
||||
)
|
||||
},
|
||||
c[Calendar.YEAR],
|
||||
c[Calendar.MONTH],
|
||||
c[Calendar.DAY_OF_MONTH],
|
||||
)
|
||||
dialog.showYearPickerFirst(true)
|
||||
fragmentManager.beginTransaction().add(dialog, null).commit()
|
||||
}
|
||||
|
||||
birthDateView.setOnClickListener {
|
||||
val c = loadDate(birthDateView)
|
||||
val dialog = DatePickerDialog.newInstance(
|
||||
{ _, year, monthOfYear, dayOfMonth ->
|
||||
saveDate(birthDateView, year, monthOfYear, dayOfMonth, KEY_BIRTH_DATE)
|
||||
},
|
||||
c[Calendar.YEAR],
|
||||
c[Calendar.MONTH],
|
||||
c[Calendar.DAY_OF_MONTH],
|
||||
)
|
||||
dialog.showYearPickerFirst(true)
|
||||
fragmentManager.beginTransaction().add(dialog, null).commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val adapter = NfcAdapter.getDefaultAdapter(this)
|
||||
if (adapter != null) {
|
||||
val intent = Intent(applicationContext, this.javaClass)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
|
||||
val filter = arrayOf(arrayOf("android.nfc.tech.IsoDep"))
|
||||
adapter.enableForegroundDispatch(this, pendingIntent, null, filter)
|
||||
}
|
||||
if (passportNumberFromIntent) {
|
||||
// When the passport number field is populated from the caller, we hide the
|
||||
// soft keyboard as otherwise it can obscure the 'Reading data' progress indicator.
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
val adapter = NfcAdapter.getDefaultAdapter(this)
|
||||
adapter?.disableForegroundDispatch(this)
|
||||
}
|
||||
|
||||
public override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
|
||||
val tag: Tag? = intent.extras?.getParcelable(NfcAdapter.EXTRA_TAG)
|
||||
if (tag?.techList?.contains("android.nfc.tech.IsoDep") == true) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val passportNumber = preferences.getString(KEY_PASSPORT_NUMBER, null)
|
||||
val expirationDate = convertDate(preferences.getString(KEY_EXPIRATION_DATE, null))
|
||||
val birthDate = convertDate(preferences.getString(KEY_BIRTH_DATE, null))
|
||||
if (!passportNumber.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && !birthDate.isNullOrEmpty()) {
|
||||
val bacKey: BACKeySpec = BACKey(passportNumber, birthDate, expirationDate)
|
||||
ReadTask(IsoDep.get(tag), bacKey).execute()
|
||||
mainLayout.visibility = View.GONE
|
||||
loadingLayout.visibility = View.VISIBLE
|
||||
} else {
|
||||
Snackbar.make(passportNumberView, R.string.error_input, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private inner class ReadTask(private val isoDep: IsoDep, private val bacKey: BACKeySpec) : AsyncTask<Void?, Void?, Exception?>() {
|
||||
|
||||
private lateinit var dg1File: DG1File
|
||||
private lateinit var dg2File: DG2File
|
||||
private lateinit var dg14File: DG14File
|
||||
private lateinit var sodFile: SODFile
|
||||
private var imageBase64: String? = null
|
||||
private var bitmap: Bitmap? = null
|
||||
private var chipAuthSucceeded = false
|
||||
private var passiveAuthSuccess = false
|
||||
private lateinit var dg14Encoded: ByteArray
|
||||
|
||||
override fun doInBackground(vararg params: Void?): Exception? {
|
||||
try {
|
||||
isoDep.timeout = 10000
|
||||
val cardService = CardService.getInstance(isoDep)
|
||||
cardService.open()
|
||||
val service = PassportService(
|
||||
cardService,
|
||||
PassportService.NORMAL_MAX_TRANCEIVE_LENGTH,
|
||||
PassportService.DEFAULT_MAX_BLOCKSIZE,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
service.open()
|
||||
var paceSucceeded = false
|
||||
try {
|
||||
val cardAccessFile = CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS))
|
||||
val securityInfoCollection = cardAccessFile.securityInfos
|
||||
for (securityInfo: SecurityInfo in securityInfoCollection) {
|
||||
if (securityInfo is PACEInfo) {
|
||||
service.doPACE(
|
||||
bacKey,
|
||||
securityInfo.objectIdentifier,
|
||||
PACEInfo.toParameterSpec(securityInfo.parameterId),
|
||||
null,
|
||||
)
|
||||
paceSucceeded = true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
service.sendSelectApplet(paceSucceeded)
|
||||
if (!paceSucceeded) {
|
||||
try {
|
||||
service.getInputStream(PassportService.EF_COM).read()
|
||||
} catch (e: Exception) {
|
||||
service.doBAC(bacKey)
|
||||
}
|
||||
}
|
||||
val dg1In = service.getInputStream(PassportService.EF_DG1)
|
||||
dg1File = DG1File(dg1In)
|
||||
val dg2In = service.getInputStream(PassportService.EF_DG2)
|
||||
dg2File = DG2File(dg2In)
|
||||
val sodIn = service.getInputStream(PassportService.EF_SOD)
|
||||
sodFile = SODFile(sodIn)
|
||||
|
||||
doChipAuth(service)
|
||||
doPassiveAuth()
|
||||
|
||||
val allFaceImageInfo: MutableList<FaceImageInfo> = ArrayList()
|
||||
dg2File.faceInfos.forEach {
|
||||
allFaceImageInfo.addAll(it.faceImageInfos)
|
||||
}
|
||||
if (allFaceImageInfo.isNotEmpty()) {
|
||||
val faceImageInfo = allFaceImageInfo.first()
|
||||
val imageLength = faceImageInfo.imageLength
|
||||
val dataInputStream = DataInputStream(faceImageInfo.imageInputStream)
|
||||
val buffer = ByteArray(imageLength)
|
||||
dataInputStream.readFully(buffer, 0, imageLength)
|
||||
val inputStream: InputStream = ByteArrayInputStream(buffer, 0, imageLength)
|
||||
bitmap = decodeImage(this@MainActivity, faceImageInfo.mimeType, inputStream)
|
||||
imageBase64 = Base64.encodeToString(buffer, Base64.DEFAULT)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return e
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun doChipAuth(service: PassportService) {
|
||||
try {
|
||||
val dg14In = service.getInputStream(PassportService.EF_DG14)
|
||||
dg14Encoded = IOUtils.toByteArray(dg14In)
|
||||
val dg14InByte = ByteArrayInputStream(dg14Encoded)
|
||||
dg14File = DG14File(dg14InByte)
|
||||
val dg14FileSecurityInfo = dg14File.securityInfos
|
||||
for (securityInfo: SecurityInfo in dg14FileSecurityInfo) {
|
||||
if (securityInfo is ChipAuthenticationPublicKeyInfo) {
|
||||
service.doEACCA(
|
||||
securityInfo.keyId,
|
||||
ChipAuthenticationPublicKeyInfo.ID_CA_ECDH_AES_CBC_CMAC_256,
|
||||
securityInfo.objectIdentifier,
|
||||
securityInfo.subjectPublicKey,
|
||||
)
|
||||
chipAuthSucceeded = true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doPassiveAuth() {
|
||||
try {
|
||||
val digest = MessageDigest.getInstance(sodFile.digestAlgorithm)
|
||||
val dataHashes = sodFile.dataGroupHashes
|
||||
val dg14Hash = if (chipAuthSucceeded) digest.digest(dg14Encoded) else ByteArray(0)
|
||||
val dg1Hash = digest.digest(dg1File.encoded)
|
||||
val dg2Hash = digest.digest(dg2File.encoded)
|
||||
|
||||
if (Arrays.equals(dg1Hash, dataHashes[1]) && Arrays.equals(dg2Hash, dataHashes[2])
|
||||
&& (!chipAuthSucceeded || Arrays.equals(dg14Hash, dataHashes[14]))) {
|
||||
|
||||
val asn1InputStream = ASN1InputStream(assets.open("masterList"))
|
||||
val keystore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keystore.load(null, null)
|
||||
val cf = CertificateFactory.getInstance("X.509")
|
||||
|
||||
var p: ASN1Primitive?
|
||||
while (asn1InputStream.readObject().also { p = it } != null) {
|
||||
val asn1 = ASN1Sequence.getInstance(p)
|
||||
if (asn1 == null || asn1.size() == 0) {
|
||||
throw IllegalArgumentException("Null or empty sequence passed.")
|
||||
}
|
||||
if (asn1.size() != 2) {
|
||||
throw IllegalArgumentException("Incorrect sequence size: " + asn1.size())
|
||||
}
|
||||
val certSet = ASN1Set.getInstance(asn1.getObjectAt(1))
|
||||
for (i in 0 until certSet.size()) {
|
||||
val certificate = Certificate.getInstance(certSet.getObjectAt(i))
|
||||
val pemCertificate = certificate.encoded
|
||||
val javaCertificate = cf.generateCertificate(ByteArrayInputStream(pemCertificate))
|
||||
keystore.setCertificateEntry(i.toString(), javaCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
val docSigningCertificates = sodFile.docSigningCertificates
|
||||
for (docSigningCertificate: X509Certificate in docSigningCertificates) {
|
||||
docSigningCertificate.checkValidity()
|
||||
}
|
||||
|
||||
val cp = cf.generateCertPath(docSigningCertificates)
|
||||
val pkixParameters = PKIXParameters(keystore)
|
||||
pkixParameters.isRevocationEnabled = false
|
||||
val cpv = CertPathValidator.getInstance(CertPathValidator.getDefaultType())
|
||||
cpv.validate(cp, pkixParameters)
|
||||
var sodDigestEncryptionAlgorithm = sodFile.docSigningCertificate.sigAlgName
|
||||
var isSSA = false
|
||||
if ((sodDigestEncryptionAlgorithm == "SSAwithRSA/PSS")) {
|
||||
sodDigestEncryptionAlgorithm = "SHA256withRSA/PSS"
|
||||
isSSA = true
|
||||
}
|
||||
val sign = Signature.getInstance(sodDigestEncryptionAlgorithm)
|
||||
if (isSSA) {
|
||||
sign.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1))
|
||||
}
|
||||
sign.initVerify(sodFile.docSigningCertificate)
|
||||
sign.update(sodFile.eContent)
|
||||
passiveAuthSuccess = sign.verify(sodFile.encryptedDigest)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Exception?) {
|
||||
mainLayout.visibility = View.VISIBLE
|
||||
loadingLayout.visibility = View.GONE
|
||||
if (result == null) {
|
||||
val intent = if (callingActivity != null) {
|
||||
Intent()
|
||||
} else {
|
||||
Intent(this@MainActivity, ResultActivity::class.java)
|
||||
}
|
||||
val mrzInfo = dg1File.mrzInfo
|
||||
intent.putExtra(ResultActivity.KEY_FIRST_NAME, mrzInfo.secondaryIdentifier.replace("<", " "))
|
||||
intent.putExtra(ResultActivity.KEY_LAST_NAME, mrzInfo.primaryIdentifier.replace("<", " "))
|
||||
intent.putExtra(ResultActivity.KEY_GENDER, mrzInfo.gender.toString())
|
||||
intent.putExtra(ResultActivity.KEY_STATE, mrzInfo.issuingState)
|
||||
intent.putExtra(ResultActivity.KEY_NATIONALITY, mrzInfo.nationality)
|
||||
val passiveAuthStr = if (passiveAuthSuccess) {
|
||||
getString(R.string.pass)
|
||||
} else {
|
||||
getString(R.string.failed)
|
||||
}
|
||||
val chipAuthStr = if (chipAuthSucceeded) {
|
||||
getString(R.string.pass)
|
||||
} else {
|
||||
getString(R.string.failed)
|
||||
}
|
||||
intent.putExtra(ResultActivity.KEY_PASSIVE_AUTH, passiveAuthStr)
|
||||
intent.putExtra(ResultActivity.KEY_CHIP_AUTH, chipAuthStr)
|
||||
bitmap?.let { bitmap ->
|
||||
if (encodePhotoToBase64) {
|
||||
intent.putExtra(ResultActivity.KEY_PHOTO_BASE64, imageBase64)
|
||||
} else {
|
||||
val ratio = 320.0 / bitmap.height
|
||||
val targetHeight = (bitmap.height * ratio).toInt()
|
||||
val targetWidth = (bitmap.width * ratio).toInt()
|
||||
intent.putExtra(
|
||||
ResultActivity.KEY_PHOTO,
|
||||
Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (callingActivity != null) {
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
Snackbar.make(passportNumberView, result.toString(), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertDate(input: String?): String? {
|
||||
if (input == null) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
SimpleDateFormat("yyMMdd", Locale.US).format(SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(input)!!)
|
||||
} catch (e: ParseException) {
|
||||
Log.w(MainActivity::class.java.simpleName, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDate(editText: EditText): Calendar {
|
||||
val calendar = Calendar.getInstance()
|
||||
if (editText.text.isNotEmpty()) {
|
||||
try {
|
||||
calendar.timeInMillis = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(editText.text.toString())!!.time
|
||||
} catch (e: ParseException) {
|
||||
Log.w(MainActivity::class.java.simpleName, e)
|
||||
}
|
||||
}
|
||||
return calendar
|
||||
}
|
||||
|
||||
private fun saveDate(editText: EditText, year: Int, monthOfYear: Int, dayOfMonth: Int, preferenceKey: String) {
|
||||
val value = String.format(Locale.US, "%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit().putString(preferenceKey, value).apply()
|
||||
editText.setText(value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MainActivity::class.java.simpleName
|
||||
private const val KEY_PASSPORT_NUMBER = "passportNumber"
|
||||
private const val KEY_EXPIRATION_DATE = "expirationDate"
|
||||
private const val KEY_BIRTH_DATE = "birthDate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
package com.tananaev.passportreader
|
||||
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.Security
|
||||
|
||||
class MainApplication : MultiDexApplication() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Security.insertProviderAt(BouncyCastleProvider(), 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
package com.tananaev.passportreader
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class ResultActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_result)
|
||||
findViewById<TextView>(R.id.output_first_name).text = intent.getStringExtra(KEY_FIRST_NAME)
|
||||
findViewById<TextView>(R.id.output_last_name).text = intent.getStringExtra(KEY_LAST_NAME)
|
||||
findViewById<TextView>(R.id.output_gender).text = intent.getStringExtra(KEY_GENDER)
|
||||
findViewById<TextView>(R.id.output_state).text = intent.getStringExtra(KEY_STATE)
|
||||
findViewById<TextView>(R.id.output_nationality).text = intent.getStringExtra(KEY_NATIONALITY)
|
||||
findViewById<TextView>(R.id.output_passive_auth).text = intent.getStringExtra(KEY_PASSIVE_AUTH)
|
||||
findViewById<TextView>(R.id.output_chip_auth).text = intent.getStringExtra(KEY_CHIP_AUTH)
|
||||
if (intent.hasExtra(KEY_PHOTO)) {
|
||||
@Suppress("DEPRECATION")
|
||||
findViewById<ImageView>(R.id.view_photo).setImageBitmap(intent.getParcelableExtra(KEY_PHOTO))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_FIRST_NAME = "firstName"
|
||||
const val KEY_LAST_NAME = "lastName"
|
||||
const val KEY_GENDER = "gender"
|
||||
const val KEY_STATE = "state"
|
||||
const val KEY_NATIONALITY = "nationality"
|
||||
const val KEY_PHOTO = "photo"
|
||||
const val KEY_PHOTO_BASE64 = "photoBase64"
|
||||
const val KEY_PASSIVE_AUTH = "passiveAuth"
|
||||
const val KEY_CHIP_AUTH = "chipAuth"
|
||||
}
|
||||
}
|
||||
BIN
passport-reader/app/src/main/res/drawable-xxhdpi/photo.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="270.93332"
|
||||
android:viewportHeight="270.93332">
|
||||
<path
|
||||
android:pathData="M0,-0h270.93v270.93h-270.93z"
|
||||
android:fillAlpha="1"
|
||||
android:strokeColor="#ffffff"
|
||||
android:fillColor="#009688"
|
||||
android:strokeWidth="0"
|
||||
android:strokeAlpha="1"/>
|
||||
<path
|
||||
android:pathData="M0,128.85h270.93v13.23h-270.93z"
|
||||
android:fillAlpha="1"
|
||||
android:strokeColor="#ffffff"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="0"
|
||||
android:strokeAlpha="1"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="193.5238"
|
||||
android:viewportHeight="193.5238">
|
||||
<group android:translateX="29.02857"
|
||||
android:translateY="29.02857">
|
||||
<path
|
||||
android:pathData="M67.49,66.91m-34.08,0a34.08,34.08 0,1 1,68.17 0a34.08,34.08 0,1 1,-68.17 0"
|
||||
android:fillAlpha="1"
|
||||
android:strokeColor="#ffffff"
|
||||
android:fillColor="#009688"
|
||||
android:strokeWidth="10.58333333"
|
||||
android:strokeAlpha="1"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="16dp" android:width="16dp" />
|
||||
</shape>
|
||||
107
passport-reader/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loading_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_loading" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/main_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:layout_marginBottom="@dimen/activity_vertical_margin"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
android:text="@string/info_scan_passport" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_passport_number_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input_passport_number"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/input_passport_number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_expiration_date_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input_expiration_date"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/input_expiration_date"
|
||||
android:inputType="date"
|
||||
android:focusableInTouchMode="false" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_date_of_birth_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:layout_marginBottom="@dimen/activity_vertical_margin">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input_date_of_birth"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/input_date_of_birth"
|
||||
android:inputType="date"
|
||||
android:focusableInTouchMode="false" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottom_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom" />
|
||||
|
||||
</FrameLayout>
|
||||
151
passport-reader/app/src/main/res/layout/activity_result.xml
Normal file
@@ -0,0 +1,151 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ResultActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/view_photo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="@dimen/activity_vertical_margin"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
tools:src="@drawable/photo" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:divider="@drawable/linear_divider"
|
||||
android:orientation="horizontal"
|
||||
android:showDividers="middle">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:divider="@drawable/linear_divider"
|
||||
android:gravity="right"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_first_name"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_last_name"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_gender"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_state"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_nationality"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_passive_auth"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/result_chip_auth"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:divider="@drawable/linear_divider"
|
||||
android:gravity="left"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_first_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:text="Peter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_last_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:text="Jackson" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_gender"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:text="Male" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_state"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:text="NZD" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_nationality"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:text="New Zealand" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_passive_auth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="No"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output_chip_auth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
|
||||
tools:text="No" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
passport-reader/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
passport-reader/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
passport-reader/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
passport-reader/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
passport-reader/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
</resources>
|
||||
6
passport-reader/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#009688</color>
|
||||
<color name="colorPrimaryDark">#00796B</color>
|
||||
<color name="colorAccent">#536DFE</color>
|
||||
</resources>
|
||||
4
passport-reader/app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
</resources>
|
||||
25
passport-reader/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<resources>
|
||||
|
||||
<string name="app_name">e-Passport Reader</string>
|
||||
|
||||
<string name="input_passport_number">Passport number</string>
|
||||
<string name="input_expiration_date">Expiration date</string>
|
||||
<string name="input_date_of_birth">Date of birth</string>
|
||||
|
||||
<string name="info_loading">Reading data…</string>
|
||||
<string name="info_scan_passport">Please fill the details below and place your phone on top of the passport.\n\nFollowing information is required to decrypt passport data locally. We do not store, upload or share any of your data. The app is completely open source and available for audit.</string>
|
||||
<string name="error_input">Please provide details to read passport</string>
|
||||
<string name="error_read">Failed to read passport</string>
|
||||
|
||||
<string name="result_first_name">First name</string>
|
||||
<string name="result_last_name">Last name</string>
|
||||
<string name="result_gender">Gender</string>
|
||||
<string name="result_state">Country</string>
|
||||
<string name="result_nationality">Nationality</string>
|
||||
<string name="result_passive_auth">Passive Authentication</string>
|
||||
<string name="result_chip_auth">Chip Authentication</string>
|
||||
|
||||
<string name="pass">Pass</string>
|
||||
<string name="failed">Failed</string>
|
||||
|
||||
</resources>
|
||||
9
passport-reader/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
5
passport-reader/app/src/main/res/xml/nfc_tech_filter.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<tech-list>
|
||||
<tech>android.nfc.tech.IsoDep</tech>
|
||||
</tech-list>
|
||||
</resources>
|
||||
24
passport-reader/app/src/regular/AndroidManifest.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".RegularActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="com.tananaev.passportreader.REQUEST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.tananaev.passportreader
|
||||
|
||||
class RegularActivity : MainActivity()
|
||||
24
passport-reader/build.gradle
Normal file
@@ -0,0 +1,24 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.20'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath 'com.google.gms:google-services:4.3.14'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
3
passport-reader/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
BIN
passport-reader/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
passport-reader/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
|
||||
164
passport-reader/gradlew
vendored
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# 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
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# 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\"`/" >&-
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >&-
|
||||
|
||||
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" ] ; 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, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
# 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=$((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
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
90
passport-reader/gradlew.bat
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
@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
|
||||
|
||||
@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=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@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 init
|
||||
|
||||
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 init
|
||||
|
||||
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
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
: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 %CMD_LINE_ARGS%
|
||||
|
||||
: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
|
||||
76
passport-reader/icon.svg
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="512mm"
|
||||
height="512mm"
|
||||
viewBox="0 0 512 512"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91+devel+osxmenu r12922"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/Users/user/Documents/passport-reader/icon.png"
|
||||
inkscape:export-xdpi="50.799999"
|
||||
inkscape:export-ydpi="50.799999">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="972.88293"
|
||||
inkscape:cy="902.85714"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1155"
|
||||
inkscape:window-x="4"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,215)">
|
||||
<g
|
||||
id="g3348"
|
||||
transform="matrix(4.0506448,0,0,4.0506448,43.341148,-76.468698)"
|
||||
style="fill:#009688;fill-opacity:1">
|
||||
<path
|
||||
id="path3338"
|
||||
d="m 5,5 0,21.5 31.71875,0 C 37.92598,18.860978 44.52329,13 52.5,13 c 7.97671,10e-7 14.57402,5.860978 15.78125,13.5 L 100,26.5 100,5 Z m 0,26.5 0,21.5 95,0 0,-21.5 -31.71875,0 C 67.07402,39.139022 60.47671,45 52.5,45 44.52329,44.999999 37.92598,39.139022 36.71875,31.5 Z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#009688;fill-opacity:1" />
|
||||
<circle
|
||||
id="circle3340"
|
||||
r="11"
|
||||
cy="29"
|
||||
cx="52.5"
|
||||
style="fill:#009688;fill-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1342
passport-reader/legacy/Passport.java
Normal file
1
passport-reader/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
include ':app'
|
||||