re-added passport-reader

This commit is contained in:
Youssef El Saadany
2023-07-22 14:28:32 +02:00
parent 338f20544b
commit 77490b59b5
46 changed files with 2963 additions and 6 deletions

6
.gitmodules vendored
View File

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

View 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

View 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
View File

@@ -0,0 +1,7 @@
.gradle
.idea
.DS_Store
local.properties
google-services.json
*.iml
build

View 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
View File

@@ -0,0 +1,36 @@
# e-Passport NFC Reader
[![Get it on Google Play](http://www.tananaev.com/badges/google-play.svg)](https://play.google.com/store/apps/details?id=com.tananaev.passportreader) [![Get it on F-Droid](http://www.tananaev.com/badges/f-droid.svg)](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.

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

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

View File

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

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

View File

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

View File

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

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

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

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.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

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

View File

@@ -0,0 +1,4 @@
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

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

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

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

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

View File

@@ -0,0 +1,3 @@
package com.tananaev.passportreader
class RegularActivity : MainActivity()

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

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4096m
android.enableJetifier=true
android.useAndroidX=true

Binary file not shown.

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
include ':app'