├── .github
└── workflows
│ ├── android.yml
│ └── main.yml
├── .gitignore
├── PRIVACY.md
├── README.md
├── app
├── build.gradle
└── src
│ ├── google
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── tananaev
│ │ └── passportreader
│ │ └── GoogleActivity.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── masterList
│ ├── ic_launcher-web.png
│ ├── java
│ │ └── com
│ │ │ └── tananaev
│ │ │ └── passportreader
│ │ │ ├── ImageUtil.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainApplication.kt
│ │ │ └── ResultActivity.kt
│ └── res
│ │ ├── drawable-xxhdpi
│ │ └── photo.png
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_launcher_monochrome.xml
│ │ └── linear_divider.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── activity_result.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── nfc_tech_filter.xml
│ └── regular
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── tananaev
│ └── passportreader
│ └── RegularActivity.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── icon.svg
├── legacy
└── Passport.java
└── settings.gradle
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: actions/setup-java@v3
17 | with:
18 | distribution: 'temurin'
19 | java-version: '17'
20 | - run: ./gradlew :app:assembleRegularDebug
21 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Update Master List
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 1 * *'
6 |
7 | jobs:
8 | update:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 |
13 | - name: Update MasterList file
14 | 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
15 |
16 | - name: Commit new masterList
17 | run: |
18 | set +e
19 | git add .
20 | git config user.name "$(git --no-pager log --format=format:'%an' -n 1)"
21 | git config user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
22 | git commit -m "Update masterList"
23 | git push "https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY"
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | .DS_Store
4 | local.properties
5 | google-services.json
6 | *.iml
7 | build
8 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | 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.
4 |
5 | # Contact
6 |
7 | If you have any questions or concerns, please feel free to contact us via GitHub issues.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # e-Passport NFC Reader
2 |
3 | [](https://play.google.com/store/apps/details?id=com.tananaev.passportreader) [](https://f-droid.org/packages/com.tananaev.passportreader)
4 |
5 | Android app that uses the NFC chip to communicate with an electronic passport.
6 |
7 | ## Contacts
8 |
9 | Author - Anton Tananaev ([anton.tananaev@gmail.com](mailto:anton.tananaev@gmail.com))
10 |
11 | ## Dependencies
12 |
13 | Note that the app includes following third party dependencies:
14 |
15 | - JMRTD - [LGPL 3.0 License](https://www.gnu.org/licenses/lgpl-3.0.en.html)
16 | - SCUBA (Smart Card Utils) - [LGPL 3.0 License](https://www.gnu.org/licenses/lgpl-3.0.en.html)
17 | - Spongy Castle - MIT-based [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html)
18 | - JP2 for Android - [BSD 2-Clause License](https://opensource.org/licenses/BSD-2-Clause)
19 | - JNBIS - [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0)
20 | - Material DateTimepicker - [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0)
21 |
22 | ## License
23 |
24 | Apache License, Version 2.0
25 |
26 | Licensed under the Apache License, Version 2.0 (the "License");
27 | you may not use this file except in compliance with the License.
28 | You may obtain a copy of the License at
29 |
30 | http://www.apache.org/licenses/LICENSE-2.0
31 |
32 | Unless required by applicable law or agreed to in writing, software
33 | distributed under the License is distributed on an "AS IS" BASIS,
34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
35 | See the License for the specific language governing permissions and
36 | limitations under the License.
37 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'com.google.gms.google-services'
5 | id 'com.google.firebase.crashlytics'
6 | }
7 |
8 | android {
9 | compileSdk 34
10 | ndkVersion '23.1.7779620'
11 | defaultConfig {
12 | applicationId 'com.tananaev.passportreader'
13 | minSdkVersion 19
14 | targetSdkVersion 34
15 | versionCode 20
16 | versionName '3.1'
17 | multiDexEnabled = true
18 | }
19 | namespace 'com.tananaev.passportreader'
20 |
21 | buildFeatures {
22 | flavorDimensions = ['default']
23 | }
24 | productFlavors {
25 | regular {
26 | isDefault = true
27 | ext.enableCrashlytics = false
28 | }
29 | google
30 | }
31 |
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_17
34 | targetCompatibility = JavaVersion.VERSION_17
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = JavaVersion.VERSION_17.toString()
39 | }
40 |
41 | packagingOptions {
42 | resources {
43 | excludes += ['META-INF/LICENSE', 'META-INF/NOTICE']
44 | }
45 | }
46 | }
47 |
48 | dependencies {
49 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
50 | implementation 'androidx.multidex:multidex:2.0.1'
51 | implementation 'com.google.android.material:material:1.10.0'
52 | implementation 'androidx.core:core-ktx:1.12.0'
53 | implementation 'com.wdullaer:materialdatetimepicker:3.5.2'
54 | implementation 'org.jmrtd:jmrtd:0.7.18'
55 | implementation 'net.sf.scuba:scuba-sc-android:0.0.18'
56 | implementation 'com.madgag.spongycastle:prov:1.54.0.0'
57 | implementation 'com.gemalto.jp2:jp2-android:1.0.3'
58 | implementation 'com.github.mhshams:jnbis:1.1.0'
59 | implementation 'org.bouncycastle:bcpkix-jdk15on:1.65' // do not update
60 | implementation 'commons-io:commons-io:2.11.0'
61 | googleImplementation platform('com.google.firebase:firebase-bom:32.5.0')
62 | googleImplementation 'com.google.firebase:firebase-analytics-ktx'
63 | googleImplementation 'com.google.firebase:firebase-crashlytics'
64 | googleImplementation 'com.google.android.gms:play-services-ads:22.5.0'
65 | googleImplementation 'com.google.android.play:review-ktx:2.0.1'
66 | }
67 |
68 | tasks.register('copyFirebaseConfig', Copy) {
69 | from '../../environment/firebase'
70 | into '.'
71 | include 'passport-reader.json'
72 | rename('passport-reader.json', 'google-services.json')
73 | }
74 | afterEvaluate {
75 | tasks.matching { it.name.contains('Google') }.configureEach { task ->
76 | if (task.name.contains('Regular')) {
77 | task.enabled false
78 | } else {
79 | task.dependsOn copyFirebaseConfig
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/google/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/google/java/com/tananaev/passportreader/GoogleActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tananaev.passportreader
2 |
3 | import android.os.Bundle
4 | import android.preference.PreferenceManager
5 | import android.widget.FrameLayout
6 | import com.google.android.gms.ads.AdRequest
7 | import com.google.android.gms.ads.AdSize
8 | import com.google.android.gms.ads.AdView
9 | import com.google.android.gms.ads.MobileAds
10 | import com.google.android.play.core.review.ReviewManagerFactory
11 | import com.google.firebase.analytics.FirebaseAnalytics
12 | import com.google.firebase.analytics.ktx.analytics
13 | import com.google.firebase.ktx.Firebase
14 |
15 | class GoogleActivity : MainActivity() {
16 | private lateinit var firebaseAnalytics: FirebaseAnalytics
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | firebaseAnalytics = Firebase.analytics
21 | MobileAds.initialize(this) {}
22 |
23 | val adView = AdView(this).apply {
24 | setAdSize(AdSize.BANNER)
25 | adUnitId = "ca-app-pub-9061647223840223/5869276959"
26 | loadAd(AdRequest.Builder().build())
27 | }
28 | val params = FrameLayout.LayoutParams(
29 | FrameLayout.LayoutParams.MATCH_PARENT,
30 | FrameLayout.LayoutParams.WRAP_CONTENT,
31 | )
32 | val containerView: FrameLayout = findViewById(R.id.bottom_container)
33 | containerView.addView(adView, params)
34 | }
35 |
36 | override fun onResume() {
37 | super.onResume()
38 | handleRating()
39 | }
40 |
41 | @Suppress("DEPRECATION")
42 | private fun handleRating() {
43 | val preferences = PreferenceManager.getDefaultSharedPreferences(this)
44 | if (!preferences.getBoolean("ratingShown", false)) {
45 | val openTimes = preferences.getInt("openTimes", 0) + 1
46 | preferences.edit().putInt("openTimes", openTimes).apply()
47 | if (openTimes >= 5) {
48 | val reviewManager = ReviewManagerFactory.create(this)
49 | reviewManager.requestReviewFlow().addOnCompleteListener { infoTask ->
50 | if (infoTask.isSuccessful) {
51 | val flow = reviewManager.launchReviewFlow(this, infoTask.result)
52 | flow.addOnCompleteListener {
53 | preferences.edit().putBoolean("ratingShown", true).apply()
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/assets/masterList:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/assets/masterList
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/com/tananaev/passportreader/ImageUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.tananaev.passportreader
17 |
18 | import android.content.Context
19 | import android.graphics.Bitmap
20 | import android.graphics.BitmapFactory
21 | import com.gemalto.jp2.JP2Decoder
22 | import org.jnbis.WsqDecoder
23 | import java.io.InputStream
24 |
25 | object ImageUtil {
26 |
27 | fun decodeImage(context: Context?, mimeType: String, inputStream: InputStream?): Bitmap {
28 | return if (mimeType.equals("image/jp2", ignoreCase = true) || mimeType.equals(
29 | "image/jpeg2000",
30 | ignoreCase = true
31 | )
32 | ) {
33 | JP2Decoder(inputStream).decode()
34 | } else if (mimeType.equals("image/x-wsq", ignoreCase = true)) {
35 | val wsqDecoder = WsqDecoder()
36 | val bitmap = wsqDecoder.decode(inputStream)
37 | val byteData = bitmap.pixels
38 | val intData = IntArray(byteData.size)
39 | for (j in byteData.indices) {
40 | intData[j] = 0xFF000000.toInt() or
41 | (byteData[j].toInt() and 0xFF shl 16) or
42 | (byteData[j].toInt() and 0xFF shl 8) or
43 | (byteData[j].toInt() and 0xFF)
44 | }
45 | Bitmap.createBitmap(
46 | intData,
47 | 0,
48 | bitmap.width,
49 | bitmap.width,
50 | bitmap.height,
51 | Bitmap.Config.ARGB_8888
52 | )
53 | } else {
54 | BitmapFactory.decodeStream(inputStream)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tananaev/passportreader/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | @file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
17 |
18 | package com.tananaev.passportreader
19 |
20 | import android.annotation.SuppressLint
21 | import android.app.PendingIntent
22 | import android.content.Intent
23 | import android.graphics.Bitmap
24 | import android.nfc.NfcAdapter
25 | import android.nfc.Tag
26 | import android.nfc.tech.IsoDep
27 | import android.os.AsyncTask
28 | import android.os.Bundle
29 | import android.preference.PreferenceManager
30 | import android.text.Editable
31 | import android.text.TextWatcher
32 | import android.util.Base64
33 | import android.util.Log
34 | import android.view.View
35 | import android.view.WindowManager
36 | import android.widget.EditText
37 | import androidx.appcompat.app.AppCompatActivity
38 | import com.google.android.material.snackbar.Snackbar
39 | import com.tananaev.passportreader.ImageUtil.decodeImage
40 | import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
41 | import net.sf.scuba.smartcards.CardService
42 | import org.apache.commons.io.IOUtils
43 | import org.bouncycastle.asn1.ASN1InputStream
44 | import org.bouncycastle.asn1.ASN1Primitive
45 | import org.bouncycastle.asn1.ASN1Sequence
46 | import org.bouncycastle.asn1.ASN1Set
47 | import org.bouncycastle.asn1.x509.Certificate
48 | import org.jmrtd.BACKey
49 | import org.jmrtd.BACKeySpec
50 | import org.jmrtd.PassportService
51 | import org.jmrtd.lds.CardAccessFile
52 | import org.jmrtd.lds.ChipAuthenticationPublicKeyInfo
53 | import org.jmrtd.lds.PACEInfo
54 | import org.jmrtd.lds.SODFile
55 | import org.jmrtd.lds.SecurityInfo
56 | import org.jmrtd.lds.icao.DG14File
57 | import org.jmrtd.lds.icao.DG1File
58 | import org.jmrtd.lds.icao.DG2File
59 | import org.jmrtd.lds.iso19794.FaceImageInfo
60 | import java.io.ByteArrayInputStream
61 | import java.io.DataInputStream
62 | import java.io.InputStream
63 | import java.security.KeyStore
64 | import java.security.MessageDigest
65 | import java.security.Signature
66 | import java.security.cert.CertPathValidator
67 | import java.security.cert.CertificateFactory
68 | import java.security.cert.PKIXParameters
69 | import java.security.cert.X509Certificate
70 | import java.security.spec.MGF1ParameterSpec
71 | import java.security.spec.PSSParameterSpec
72 | import java.text.ParseException
73 | import java.text.SimpleDateFormat
74 | import java.util.*
75 |
76 | abstract class MainActivity : AppCompatActivity() {
77 |
78 | private lateinit var passportNumberView: EditText
79 | private lateinit var expirationDateView: EditText
80 | private lateinit var birthDateView: EditText
81 | private var passportNumberFromIntent = false
82 | private var encodePhotoToBase64 = false
83 | private lateinit var mainLayout: View
84 | private lateinit var loadingLayout: View
85 |
86 | override fun onCreate(savedInstanceState: Bundle?) {
87 | super.onCreate(savedInstanceState)
88 | setContentView(R.layout.activity_main)
89 |
90 | val preferences = PreferenceManager.getDefaultSharedPreferences(this)
91 | val dateOfBirth = intent.getStringExtra("dateOfBirth")
92 | val dateOfExpiry = intent.getStringExtra("dateOfExpiry")
93 | val passportNumber = intent.getStringExtra("passportNumber")
94 | encodePhotoToBase64 = intent.getBooleanExtra("photoAsBase64", false)
95 | if (dateOfBirth != null) {
96 | PreferenceManager.getDefaultSharedPreferences(this)
97 | .edit().putString(KEY_BIRTH_DATE, dateOfBirth).apply()
98 | }
99 | if (dateOfExpiry != null) {
100 | PreferenceManager.getDefaultSharedPreferences(this)
101 | .edit().putString(KEY_EXPIRATION_DATE, dateOfExpiry).apply()
102 | }
103 | if (passportNumber != null) {
104 | PreferenceManager.getDefaultSharedPreferences(this)
105 | .edit().putString(KEY_PASSPORT_NUMBER, passportNumber).apply()
106 | passportNumberFromIntent = true
107 | }
108 |
109 | passportNumberView = findViewById(R.id.input_passport_number)
110 | expirationDateView = findViewById(R.id.input_expiration_date)
111 | birthDateView = findViewById(R.id.input_date_of_birth)
112 | mainLayout = findViewById(R.id.main_layout)
113 | loadingLayout = findViewById(R.id.loading_layout)
114 |
115 | passportNumberView.setText(preferences.getString(KEY_PASSPORT_NUMBER, null))
116 | expirationDateView.setText(preferences.getString(KEY_EXPIRATION_DATE, null))
117 | birthDateView.setText(preferences.getString(KEY_BIRTH_DATE, null))
118 |
119 | passportNumberView.addTextChangedListener(object : TextWatcher {
120 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
121 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
122 | override fun afterTextChanged(s: Editable) {
123 | PreferenceManager.getDefaultSharedPreferences(this@MainActivity)
124 | .edit().putString(KEY_PASSPORT_NUMBER, s.toString()).apply()
125 | }
126 | })
127 |
128 | expirationDateView.setOnClickListener {
129 | val c = loadDate(expirationDateView)
130 | val dialog = DatePickerDialog.newInstance(
131 | { _, year, monthOfYear, dayOfMonth ->
132 | saveDate(
133 | expirationDateView,
134 | year,
135 | monthOfYear,
136 | dayOfMonth,
137 | KEY_EXPIRATION_DATE,
138 | )
139 | },
140 | c[Calendar.YEAR],
141 | c[Calendar.MONTH],
142 | c[Calendar.DAY_OF_MONTH],
143 | )
144 | dialog.showYearPickerFirst(true)
145 | fragmentManager.beginTransaction().add(dialog, null).commit()
146 | }
147 |
148 | birthDateView.setOnClickListener {
149 | val c = loadDate(birthDateView)
150 | val dialog = DatePickerDialog.newInstance(
151 | { _, year, monthOfYear, dayOfMonth ->
152 | saveDate(birthDateView, year, monthOfYear, dayOfMonth, KEY_BIRTH_DATE)
153 | },
154 | c[Calendar.YEAR],
155 | c[Calendar.MONTH],
156 | c[Calendar.DAY_OF_MONTH],
157 | )
158 | dialog.showYearPickerFirst(true)
159 | fragmentManager.beginTransaction().add(dialog, null).commit()
160 | }
161 | }
162 |
163 | override fun onResume() {
164 | super.onResume()
165 | val adapter = NfcAdapter.getDefaultAdapter(this)
166 | if (adapter != null) {
167 | val intent = Intent(applicationContext, this.javaClass)
168 | intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
169 | val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
170 | val filter = arrayOf(arrayOf("android.nfc.tech.IsoDep"))
171 | adapter.enableForegroundDispatch(this, pendingIntent, null, filter)
172 | }
173 | if (passportNumberFromIntent) {
174 | // When the passport number field is populated from the caller, we hide the
175 | // soft keyboard as otherwise it can obscure the 'Reading data' progress indicator.
176 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
177 | }
178 | }
179 |
180 | override fun onPause() {
181 | super.onPause()
182 | val adapter = NfcAdapter.getDefaultAdapter(this)
183 | adapter?.disableForegroundDispatch(this)
184 | }
185 |
186 | public override fun onNewIntent(intent: Intent) {
187 | super.onNewIntent(intent)
188 | if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
189 | val tag: Tag? = intent.extras?.getParcelable(NfcAdapter.EXTRA_TAG)
190 | if (tag?.techList?.contains("android.nfc.tech.IsoDep") == true) {
191 | val preferences = PreferenceManager.getDefaultSharedPreferences(this)
192 | val passportNumber = preferences.getString(KEY_PASSPORT_NUMBER, null)
193 | val expirationDate = convertDate(preferences.getString(KEY_EXPIRATION_DATE, null))
194 | val birthDate = convertDate(preferences.getString(KEY_BIRTH_DATE, null))
195 | if (!passportNumber.isNullOrEmpty() && !expirationDate.isNullOrEmpty() && !birthDate.isNullOrEmpty()) {
196 | val bacKey: BACKeySpec = BACKey(passportNumber, birthDate, expirationDate)
197 | ReadTask(IsoDep.get(tag), bacKey).execute()
198 | mainLayout.visibility = View.GONE
199 | loadingLayout.visibility = View.VISIBLE
200 | } else {
201 | Snackbar.make(passportNumberView, R.string.error_input, Snackbar.LENGTH_SHORT).show()
202 | }
203 | }
204 | }
205 | }
206 |
207 | @SuppressLint("StaticFieldLeak")
208 | private inner class ReadTask(private val isoDep: IsoDep, private val bacKey: BACKeySpec) : AsyncTask() {
209 |
210 | private lateinit var dg1File: DG1File
211 | private lateinit var dg2File: DG2File
212 | private lateinit var dg14File: DG14File
213 | private lateinit var sodFile: SODFile
214 | private var imageBase64: String? = null
215 | private var bitmap: Bitmap? = null
216 | private var chipAuthSucceeded = false
217 | private var passiveAuthSuccess = false
218 | private lateinit var dg14Encoded: ByteArray
219 |
220 | override fun doInBackground(vararg params: Void?): Exception? {
221 | try {
222 | isoDep.timeout = 10000
223 | val cardService = CardService.getInstance(isoDep)
224 | cardService.open()
225 | val service = PassportService(
226 | cardService,
227 | PassportService.NORMAL_MAX_TRANCEIVE_LENGTH,
228 | PassportService.DEFAULT_MAX_BLOCKSIZE,
229 | false,
230 | false,
231 | )
232 | service.open()
233 | var paceSucceeded = false
234 | try {
235 | val cardAccessFile = CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS))
236 | val securityInfoCollection = cardAccessFile.securityInfos
237 | for (securityInfo: SecurityInfo in securityInfoCollection) {
238 | if (securityInfo is PACEInfo) {
239 | service.doPACE(
240 | bacKey,
241 | securityInfo.objectIdentifier,
242 | PACEInfo.toParameterSpec(securityInfo.parameterId),
243 | null,
244 | )
245 | paceSucceeded = true
246 | }
247 | }
248 | } catch (e: Exception) {
249 | Log.w(TAG, e)
250 | }
251 | service.sendSelectApplet(paceSucceeded)
252 | if (!paceSucceeded) {
253 | try {
254 | service.getInputStream(PassportService.EF_COM).read()
255 | } catch (e: Exception) {
256 | service.doBAC(bacKey)
257 | }
258 | }
259 | val dg1In = service.getInputStream(PassportService.EF_DG1)
260 | dg1File = DG1File(dg1In)
261 | val dg2In = service.getInputStream(PassportService.EF_DG2)
262 | dg2File = DG2File(dg2In)
263 | val sodIn = service.getInputStream(PassportService.EF_SOD)
264 | sodFile = SODFile(sodIn)
265 |
266 | doChipAuth(service)
267 | doPassiveAuth()
268 |
269 | val allFaceImageInfo: MutableList = ArrayList()
270 | dg2File.faceInfos.forEach {
271 | allFaceImageInfo.addAll(it.faceImageInfos)
272 | }
273 | if (allFaceImageInfo.isNotEmpty()) {
274 | val faceImageInfo = allFaceImageInfo.first()
275 | val imageLength = faceImageInfo.imageLength
276 | val dataInputStream = DataInputStream(faceImageInfo.imageInputStream)
277 | val buffer = ByteArray(imageLength)
278 | dataInputStream.readFully(buffer, 0, imageLength)
279 | val inputStream: InputStream = ByteArrayInputStream(buffer, 0, imageLength)
280 | bitmap = decodeImage(this@MainActivity, faceImageInfo.mimeType, inputStream)
281 | imageBase64 = Base64.encodeToString(buffer, Base64.DEFAULT)
282 | }
283 | } catch (e: Exception) {
284 | return e
285 | }
286 | return null
287 | }
288 |
289 | private fun doChipAuth(service: PassportService) {
290 | try {
291 | val dg14In = service.getInputStream(PassportService.EF_DG14)
292 | dg14Encoded = IOUtils.toByteArray(dg14In)
293 | val dg14InByte = ByteArrayInputStream(dg14Encoded)
294 | dg14File = DG14File(dg14InByte)
295 | val dg14FileSecurityInfo = dg14File.securityInfos
296 | for (securityInfo: SecurityInfo in dg14FileSecurityInfo) {
297 | if (securityInfo is ChipAuthenticationPublicKeyInfo) {
298 | service.doEACCA(
299 | securityInfo.keyId,
300 | ChipAuthenticationPublicKeyInfo.ID_CA_ECDH_AES_CBC_CMAC_256,
301 | securityInfo.objectIdentifier,
302 | securityInfo.subjectPublicKey,
303 | )
304 | chipAuthSucceeded = true
305 | }
306 | }
307 | } catch (e: Exception) {
308 | Log.w(TAG, e)
309 | }
310 | }
311 |
312 | private fun doPassiveAuth() {
313 | try {
314 | val digest = MessageDigest.getInstance(sodFile.digestAlgorithm)
315 | val dataHashes = sodFile.dataGroupHashes
316 | val dg14Hash = if (chipAuthSucceeded) digest.digest(dg14Encoded) else ByteArray(0)
317 | val dg1Hash = digest.digest(dg1File.encoded)
318 | val dg2Hash = digest.digest(dg2File.encoded)
319 |
320 | if (Arrays.equals(dg1Hash, dataHashes[1]) && Arrays.equals(dg2Hash, dataHashes[2])
321 | && (!chipAuthSucceeded || Arrays.equals(dg14Hash, dataHashes[14]))) {
322 |
323 | val asn1InputStream = ASN1InputStream(assets.open("masterList"))
324 | val keystore = KeyStore.getInstance(KeyStore.getDefaultType())
325 | keystore.load(null, null)
326 | val cf = CertificateFactory.getInstance("X.509")
327 |
328 | var p: ASN1Primitive?
329 | while (asn1InputStream.readObject().also { p = it } != null) {
330 | val asn1 = ASN1Sequence.getInstance(p)
331 | if (asn1 == null || asn1.size() == 0) {
332 | throw IllegalArgumentException("Null or empty sequence passed.")
333 | }
334 | if (asn1.size() != 2) {
335 | throw IllegalArgumentException("Incorrect sequence size: " + asn1.size())
336 | }
337 | val certSet = ASN1Set.getInstance(asn1.getObjectAt(1))
338 | for (i in 0 until certSet.size()) {
339 | val certificate = Certificate.getInstance(certSet.getObjectAt(i))
340 | val pemCertificate = certificate.encoded
341 | val javaCertificate = cf.generateCertificate(ByteArrayInputStream(pemCertificate))
342 | keystore.setCertificateEntry(i.toString(), javaCertificate)
343 | }
344 | }
345 |
346 | val docSigningCertificates = sodFile.docSigningCertificates
347 | for (docSigningCertificate: X509Certificate in docSigningCertificates) {
348 | docSigningCertificate.checkValidity()
349 | }
350 |
351 | val cp = cf.generateCertPath(docSigningCertificates)
352 | val pkixParameters = PKIXParameters(keystore)
353 | pkixParameters.isRevocationEnabled = false
354 | val cpv = CertPathValidator.getInstance(CertPathValidator.getDefaultType())
355 | cpv.validate(cp, pkixParameters)
356 | var sodDigestEncryptionAlgorithm = sodFile.docSigningCertificate.sigAlgName
357 | var isSSA = false
358 | if ((sodDigestEncryptionAlgorithm == "SSAwithRSA/PSS")) {
359 | sodDigestEncryptionAlgorithm = "SHA256withRSA/PSS"
360 | isSSA = true
361 | }
362 | val sign = Signature.getInstance(sodDigestEncryptionAlgorithm)
363 | if (isSSA) {
364 | sign.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1))
365 | }
366 | sign.initVerify(sodFile.docSigningCertificate)
367 | sign.update(sodFile.eContent)
368 | passiveAuthSuccess = sign.verify(sodFile.encryptedDigest)
369 | }
370 | } catch (e: Exception) {
371 | Log.w(TAG, e)
372 | }
373 | }
374 |
375 | override fun onPostExecute(result: Exception?) {
376 | mainLayout.visibility = View.VISIBLE
377 | loadingLayout.visibility = View.GONE
378 | if (result == null) {
379 | val intent = if (callingActivity != null) {
380 | Intent()
381 | } else {
382 | Intent(this@MainActivity, ResultActivity::class.java)
383 | }
384 | val mrzInfo = dg1File.mrzInfo
385 | intent.putExtra(ResultActivity.KEY_FIRST_NAME, mrzInfo.secondaryIdentifier.replace("<", " "))
386 | intent.putExtra(ResultActivity.KEY_LAST_NAME, mrzInfo.primaryIdentifier.replace("<", " "))
387 | intent.putExtra(ResultActivity.KEY_GENDER, mrzInfo.gender.toString())
388 | intent.putExtra(ResultActivity.KEY_STATE, mrzInfo.issuingState)
389 | intent.putExtra(ResultActivity.KEY_NATIONALITY, mrzInfo.nationality)
390 | val passiveAuthStr = if (passiveAuthSuccess) {
391 | getString(R.string.pass)
392 | } else {
393 | getString(R.string.failed)
394 | }
395 | val chipAuthStr = if (chipAuthSucceeded) {
396 | getString(R.string.pass)
397 | } else {
398 | getString(R.string.failed)
399 | }
400 | intent.putExtra(ResultActivity.KEY_PASSIVE_AUTH, passiveAuthStr)
401 | intent.putExtra(ResultActivity.KEY_CHIP_AUTH, chipAuthStr)
402 | bitmap?.let { bitmap ->
403 | if (encodePhotoToBase64) {
404 | intent.putExtra(ResultActivity.KEY_PHOTO_BASE64, imageBase64)
405 | } else {
406 | val ratio = 320.0 / bitmap.height
407 | val targetHeight = (bitmap.height * ratio).toInt()
408 | val targetWidth = (bitmap.width * ratio).toInt()
409 | intent.putExtra(
410 | ResultActivity.KEY_PHOTO,
411 | Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false)
412 | )
413 | }
414 | }
415 | if (callingActivity != null) {
416 | setResult(RESULT_OK, intent)
417 | finish()
418 | } else {
419 | startActivity(intent)
420 | }
421 | } else {
422 | Snackbar.make(passportNumberView, result.toString(), Snackbar.LENGTH_LONG).show()
423 | }
424 | }
425 | }
426 |
427 | private fun convertDate(input: String?): String? {
428 | if (input == null) {
429 | return null
430 | }
431 | return try {
432 | SimpleDateFormat("yyMMdd", Locale.US).format(SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(input)!!)
433 | } catch (e: ParseException) {
434 | Log.w(MainActivity::class.java.simpleName, e)
435 | null
436 | }
437 | }
438 |
439 | private fun loadDate(editText: EditText): Calendar {
440 | val calendar = Calendar.getInstance()
441 | if (editText.text.isNotEmpty()) {
442 | try {
443 | calendar.timeInMillis = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(editText.text.toString())!!.time
444 | } catch (e: ParseException) {
445 | Log.w(MainActivity::class.java.simpleName, e)
446 | }
447 | }
448 | return calendar
449 | }
450 |
451 | private fun saveDate(editText: EditText, year: Int, monthOfYear: Int, dayOfMonth: Int, preferenceKey: String) {
452 | val value = String.format(Locale.US, "%d-%02d-%02d", year, monthOfYear + 1, dayOfMonth)
453 | PreferenceManager.getDefaultSharedPreferences(this)
454 | .edit().putString(preferenceKey, value).apply()
455 | editText.setText(value)
456 | }
457 |
458 | companion object {
459 | private val TAG = MainActivity::class.java.simpleName
460 | private const val KEY_PASSPORT_NUMBER = "passportNumber"
461 | private const val KEY_EXPIRATION_DATE = "expirationDate"
462 | private const val KEY_BIRTH_DATE = "birthDate"
463 | }
464 | }
465 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tananaev/passportreader/MainApplication.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.tananaev.passportreader
17 |
18 | import androidx.multidex.MultiDexApplication
19 | import org.spongycastle.jce.provider.BouncyCastleProvider
20 | import java.security.Security
21 |
22 | class MainApplication : MultiDexApplication() {
23 | override fun onCreate() {
24 | super.onCreate()
25 | Security.insertProviderAt(BouncyCastleProvider(), 1)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tananaev/passportreader/ResultActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2022 Anton Tananaev (anton.tananaev@gmail.com)
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.tananaev.passportreader
17 |
18 | import android.os.Bundle
19 | import android.widget.ImageView
20 | import android.widget.TextView
21 | import androidx.appcompat.app.AppCompatActivity
22 |
23 | class ResultActivity : AppCompatActivity() {
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContentView(R.layout.activity_result)
27 | findViewById(R.id.output_first_name).text = intent.getStringExtra(KEY_FIRST_NAME)
28 | findViewById(R.id.output_last_name).text = intent.getStringExtra(KEY_LAST_NAME)
29 | findViewById(R.id.output_gender).text = intent.getStringExtra(KEY_GENDER)
30 | findViewById(R.id.output_state).text = intent.getStringExtra(KEY_STATE)
31 | findViewById(R.id.output_nationality).text = intent.getStringExtra(KEY_NATIONALITY)
32 | findViewById(R.id.output_passive_auth).text = intent.getStringExtra(KEY_PASSIVE_AUTH)
33 | findViewById(R.id.output_chip_auth).text = intent.getStringExtra(KEY_CHIP_AUTH)
34 | if (intent.hasExtra(KEY_PHOTO)) {
35 | @Suppress("DEPRECATION")
36 | findViewById(R.id.view_photo).setImageBitmap(intent.getParcelableExtra(KEY_PHOTO))
37 | }
38 | }
39 |
40 | companion object {
41 | const val KEY_FIRST_NAME = "firstName"
42 | const val KEY_LAST_NAME = "lastName"
43 | const val KEY_GENDER = "gender"
44 | const val KEY_STATE = "state"
45 | const val KEY_NATIONALITY = "nationality"
46 | const val KEY_PHOTO = "photo"
47 | const val KEY_PHOTO_BASE64 = "photoBase64"
48 | const val KEY_PASSIVE_AUTH = "passiveAuth"
49 | const val KEY_CHIP_AUTH = "chipAuth"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/res/drawable-xxhdpi/photo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/linear_divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
21 |
22 |
26 |
27 |
28 |
29 |
33 |
34 |
40 |
41 |
49 |
50 |
55 |
56 |
62 |
63 |
64 |
65 |
70 |
71 |
78 |
79 |
80 |
81 |
87 |
88 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_result.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
25 |
26 |
33 |
34 |
42 |
43 |
48 |
49 |
54 |
55 |
60 |
61 |
66 |
67 |
72 |
73 |
78 |
79 |
84 |
85 |
86 |
87 |
95 |
96 |
102 |
103 |
109 |
110 |
116 |
117 |
123 |
124 |
130 |
131 |
137 |
138 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 64dp
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #009688
4 | #00796B
5 | #536DFE
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 | 16dp
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | e-Passport Reader
4 |
5 | Passport number
6 | Expiration date
7 | Date of birth
8 |
9 | Reading data…
10 | 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.
11 | Please provide details to read passport
12 | Failed to read passport
13 |
14 | First name
15 | Last name
16 | Gender
17 | Country
18 | Nationality
19 | Passive Authentication
20 | Chip Authentication
21 |
22 | Pass
23 | Failed
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/nfc_tech_filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | android.nfc.tech.IsoDep
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/regular/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/regular/java/com/tananaev/passportreader/RegularActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tananaev.passportreader
2 |
3 | class RegularActivity : MainActivity()
4 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.9.20'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
9 | classpath 'com.android.tools.build:gradle:8.2.0-rc03'
10 | classpath 'com.google.gms:google-services:4.4.0'
11 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096m
2 | android.enableJetifier=true
3 | android.useAndroidX=true
4 | android.nonTransitiveRClass=false
5 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tananaev/passport-reader/846481c4a3a41e972bab76acc5d2418fd367974c/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/legacy/Passport.java:
--------------------------------------------------------------------------------
1 | /*
2 | * JMRTD - A Java API for accessing machine readable travel documents.
3 | *
4 | * Copyright (C) 2006 - 2014 The JMRTD team
5 | *
6 | * This library is free software; you can redistribute it and/or
7 | * modify it under the terms of the GNU Lesser General Public
8 | * License as published by the Free Software Foundation; either
9 | * version 2.1 of the License, or (at your option) any later version.
10 | *
11 | * This library is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | * Lesser General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU Lesser General Public
17 | * License along with this library; if not, write to the Free Software
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 | *
20 | * $Id: Passport.java 1568 2015-01-12 20:54:05Z martijno $
21 | */
22 |
23 | package org.jmrtd;
24 |
25 | import java.io.ByteArrayInputStream;
26 | import java.io.DataInputStream;
27 | import java.io.IOException;
28 | import java.io.InputStream;
29 | import java.math.BigInteger;
30 | import java.security.GeneralSecurityException;
31 | import java.security.Key;
32 | import java.security.KeyStore;
33 | import java.security.MessageDigest;
34 | import java.security.NoSuchAlgorithmException;
35 | import java.security.PrivateKey;
36 | import java.security.Provider;
37 | import java.security.PublicKey;
38 | import java.security.SecureRandom;
39 | import java.security.Security;
40 | import java.security.Signature;
41 | import java.security.cert.CertPath;
42 | import java.security.cert.CertPathBuilder;
43 | import java.security.cert.CertPathBuilderException;
44 | import java.security.cert.CertStore;
45 | import java.security.cert.CertStoreParameters;
46 | import java.security.cert.Certificate;
47 | import java.security.cert.CertificateException;
48 | import java.security.cert.CollectionCertStoreParameters;
49 | import java.security.cert.PKIXBuilderParameters;
50 | import java.security.cert.PKIXCertPathBuilderResult;
51 | import java.security.cert.TrustAnchor;
52 | import java.security.cert.X509CertSelector;
53 | import java.security.cert.X509Certificate;
54 | import java.security.interfaces.ECPublicKey;
55 | import java.security.interfaces.RSAPublicKey;
56 | import java.util.ArrayList;
57 | import java.util.Arrays;
58 | import java.util.Collection;
59 | import java.util.Collections;
60 | import java.util.List;
61 | import java.util.Map;
62 | import java.util.Random;
63 | import java.util.Set;
64 | import java.util.TreeMap;
65 | import java.util.TreeSet;
66 | import java.util.logging.Logger;
67 |
68 | import javax.crypto.Cipher;
69 | import javax.security.auth.x500.X500Principal;
70 |
71 | import net.sf.scuba.smartcards.CardFileInputStream;
72 | import net.sf.scuba.smartcards.CardServiceException;
73 |
74 | import org.bouncycastle.asn1.ASN1Encodable;
75 | import org.bouncycastle.asn1.ASN1Integer;
76 | import org.bouncycastle.asn1.ASN1Sequence;
77 | import org.bouncycastle.asn1.DERSequence;
78 | import org.jmrtd.VerificationStatus.HashMatchResult;
79 | import org.jmrtd.VerificationStatus.ReasonCode;
80 | import org.jmrtd.cert.CVCPrincipal;
81 | import org.jmrtd.cert.CardVerifiableCertificate;
82 | import org.jmrtd.lds.ActiveAuthenticationInfo;
83 | import org.jmrtd.lds.COMFile;
84 | import org.jmrtd.lds.CVCAFile;
85 | import org.jmrtd.lds.CardAccessFile;
86 | import org.jmrtd.lds.ChipAuthenticationPublicKeyInfo;
87 | import org.jmrtd.lds.DG14File;
88 | import org.jmrtd.lds.DG15File;
89 | import org.jmrtd.lds.DG1File;
90 | import org.jmrtd.lds.LDS;
91 | import org.jmrtd.lds.LDSFileUtil;
92 | import org.jmrtd.lds.PACEInfo;
93 | import org.jmrtd.lds.SODFile;
94 | import org.jmrtd.lds.SecurityInfo;
95 |
96 | /**
97 | * Contains methods for creating instances from scratch, from file, and from
98 | * card service.
99 | *
100 | * Also contains the document verification logic.
101 | *
102 | * @author Wojciech Mostowski (woj@cs.ru.nl)
103 | * @author Martijn Oostdijk (martijn.oostdijk@gmail.com)
104 | *
105 | * @version $Revision: 1568 $
106 | */
107 | public class Passport {
108 |
109 | private static final Provider BC_PROVIDER = JMRTDSecurityProvider.getBouncyCastleProvider();
110 |
111 | private final static List EMPTY_TRIED_BAC_ENTRY_LIST = Collections.emptyList();
112 | private final static List EMPTY_CERTIFICATE_CHAIN = Collections.emptyList();
113 |
114 | /** The hash function for DG hashes. */
115 | private MessageDigest digest;
116 |
117 | private FeatureStatus featureStatus;
118 | private VerificationStatus verificationStatus;
119 |
120 | /* We use a cipher to help implement Active Authentication RSA with ISO9796-2 message recovery. */
121 | private transient Signature rsaAASignature;
122 | private transient MessageDigest rsaAADigest;
123 | private transient Cipher rsaAACipher;
124 | private transient Signature ecdsaAASignature;
125 | private transient MessageDigest ecdsaAADigest;
126 |
127 | private short cvcaFID = PassportService.EF_CVCA;
128 |
129 | private LDS lds;
130 |
131 | private static final boolean IS_PKIX_REVOCATION_CHECING_ENABLED = false;
132 |
133 | private PrivateKey docSigningPrivateKey;
134 |
135 | private CardVerifiableCertificate cvcaCertificate;
136 |
137 | private PrivateKey eacPrivateKey;
138 |
139 | private PrivateKey aaPrivateKey;
140 |
141 | private static final Logger LOGGER = Logger.getLogger("org.jmrtd");
142 |
143 | /*
144 | * FIXME: replace trust store with something simpler.
145 | * - Move the URI interpretation functionality to clients.
146 | * - Limit public interface in Passport etc. to CertStore / KeyStore / ? extends Key / Certificate only.
147 | */
148 | private MRTDTrustStore trustManager;
149 |
150 | private PassportService service;
151 |
152 | private Random random;
153 |
154 | private Passport() throws GeneralSecurityException {
155 | this.featureStatus = new FeatureStatus();
156 | this.verificationStatus = new VerificationStatus();
157 |
158 | this.random = new SecureRandom();
159 |
160 | rsaAADigest = MessageDigest.getInstance("SHA1"); /* NOTE: for output length measurement only. -- MO */
161 | rsaAASignature = Signature.getInstance("SHA1WithRSA/ISO9796-2", BC_PROVIDER);
162 | rsaAACipher = Cipher.getInstance("RSA/NONE/NoPadding");
163 |
164 | /* NOTE: These will be updated in doAA after caller has read ActiveAuthenticationSecurityInfo. */
165 | ecdsaAASignature = Signature.getInstance("SHA256withECDSA", BC_PROVIDER);
166 | ecdsaAADigest = MessageDigest.getInstance("SHA-256"); /* NOTE: for output length measurement only. -- MO */
167 | }
168 |
169 | /**
170 | * Creates a document from an LDS data structure and additional information.
171 | *
172 | * @param lds the logical data structure
173 | * @param docSigningPrivateKey the document signing private key
174 | * @param trustManager the trust manager (CSCA, CVCA)
175 | *
176 | * @throws GeneralSecurityException if error
177 | */
178 | public Passport(LDS lds, PrivateKey docSigningPrivateKey, MRTDTrustStore trustManager) throws GeneralSecurityException {
179 | this();
180 | this.trustManager = trustManager;
181 | this.docSigningPrivateKey = docSigningPrivateKey;
182 | this.lds = lds;
183 | }
184 |
185 | /**
186 | * Creates a document by reading it from a service.
187 | * Access control will be BAC only.
188 | *
189 | * @param service the service to read from
190 | * @param trustManager the trust manager (CSCA, CVCA)
191 | * @param bacKey the BAC key to use
192 | *
193 | * @throws CardServiceException on error
194 | * @throws GeneralSecurityException if certain security primitives are not supported
195 | */
196 | public Passport(PassportService service, MRTDTrustStore trustManager, BACKeySpec bacKey) throws CardServiceException, GeneralSecurityException {
197 | this(service, trustManager, Collections.singletonList(bacKey), false, false);
198 | }
199 |
200 | public Passport(PassportService service, MRTDTrustStore trustManager, BACKeySpec bacKey, boolean shouldDoPACE, boolean shouldDoBACByDefault) throws CardServiceException, GeneralSecurityException {
201 | this(service, trustManager, Collections.singletonList(bacKey), shouldDoPACE, shouldDoBACByDefault);
202 | }
203 |
204 | /**
205 | * Creates a document by reading it from a service.
206 | *
207 | * @param service the service to read from
208 | * @param trustManager the trust manager (CSCA, CVCA)
209 | * @param bacStore the BAC entries
210 | * @param shouldDoPACE whether PACE should be tried before BAC
211 | * @param shouldDoBACByDefault whether BAC should be used by default and we should not expect an unprotected document
212 | *
213 | * @throws CardServiceException on error
214 | * @throws GeneralSecurityException if certain security primitives are not supported
215 | */
216 | public Passport(PassportService service, MRTDTrustStore trustManager, List bacStore, boolean shouldDoPACE, boolean shouldDoBACByDefault) throws CardServiceException, GeneralSecurityException {
217 | this();
218 | LOGGER.info("DEBUG: shouldDoBACByDefault = " + shouldDoBACByDefault);
219 | if (service == null) { throw new IllegalArgumentException("Service cannot be null"); }
220 | this.service = service;
221 | if (trustManager == null) {
222 | trustManager = new MRTDTrustStore();
223 | }
224 | this.trustManager = trustManager;
225 |
226 | boolean hasPACE = false;
227 | boolean isPACESucceeded = false;
228 | try {
229 | service.open();
230 |
231 | /* Find out whether this MRTD supports PACE. */
232 | PACEInfo paceInfo = null;
233 | try {
234 | LOGGER.info("Inspecting card access file");
235 | CardAccessFile cardAccessFile = new CardAccessFile(service.getInputStream(PassportService.EF_CARD_ACCESS));
236 | Collection paceInfos = cardAccessFile.getPACEInfos();
237 | LOGGER.info("DEBUG: found a card access file: paceInfos (" + (paceInfos == null ? 0 : paceInfos.size()) + ") = " + paceInfos);
238 |
239 | if (paceInfos != null && paceInfos.size() > 0) {
240 | /* FIXME: Multiple PACEInfos allowed? */
241 | if (paceInfos.size() > 1) { LOGGER.warning("Found multiple PACEInfos " + paceInfos.size()); }
242 | paceInfo = paceInfos.iterator().next();
243 | featureStatus.setSAC(FeatureStatus.Verdict.PRESENT);
244 | }
245 | } catch (Exception e) {
246 | /* NOTE: No card access file, continue to test for BAC. */
247 | LOGGER.info("DEBUG: failed to get card access file: " + e.getMessage());
248 | e.printStackTrace();
249 | }
250 |
251 | hasPACE = featureStatus.hasSAC() == FeatureStatus.Verdict.PRESENT;
252 |
253 | if (hasPACE && shouldDoPACE) {
254 | try {
255 | isPACESucceeded = tryToDoPACE(service, paceInfo, bacStore.get(0)); // FIXME: only one bac key, DEBUG
256 | } catch (Exception e) {
257 | e.printStackTrace();
258 | LOGGER.info("PACE failed, falling back to BAC");
259 | isPACESucceeded = false;
260 | }
261 | }
262 |
263 | LOGGER.info("DEBUG: calling select applet with isPACESucceeded = " + isPACESucceeded);
264 | service.sendSelectApplet(isPACESucceeded);
265 | } catch (CardServiceException cse) {
266 | throw cse;
267 | } catch (Exception e) {
268 | e.printStackTrace();
269 | throw new CardServiceException("Cannot open document. " + e.getMessage());
270 | }
271 |
272 | String documentNumber = null;
273 |
274 | /* If PACE did not succeed find out whether we need to do BAC. */
275 | if (!(hasPACE && isPACESucceeded)) {
276 | boolean shouldDoBAC = shouldDoBACByDefault;
277 | LOGGER.info("DEBUG: shouldDoBAC = " + shouldDoBAC);
278 |
279 | if (!shouldDoBAC) {
280 | try {
281 | /* Attempt to read EF.COM before BAC. */
282 | LOGGER.info("DEBUG: reading first byte of EF.COM");
283 | service.getInputStream(PassportService.EF_COM).read();
284 |
285 | if (isPACESucceeded) {
286 | verificationStatus.setSAC(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.SUCCEEDED);
287 | featureStatus.setBAC(FeatureStatus.Verdict.UNKNOWN);
288 | verificationStatus.setBAC(VerificationStatus.Verdict.NOT_CHECKED, ReasonCode.USING_SAC_SO_BAC_NOT_CHECKED, EMPTY_TRIED_BAC_ENTRY_LIST);
289 | } else {
290 | /* We failed PACE, and we don't need BAC. */
291 | featureStatus.setBAC(FeatureStatus.Verdict.NOT_PRESENT);
292 | verificationStatus.setBAC(VerificationStatus.Verdict.NOT_PRESENT, ReasonCode.NOT_SUPPORTED, EMPTY_TRIED_BAC_ENTRY_LIST);
293 | }
294 | } catch (Exception e) {
295 | LOGGER.info("Attempt to read EF.COM before BAC failed with: " + e.getMessage());
296 | featureStatus.setBAC(FeatureStatus.Verdict.PRESENT);
297 | verificationStatus.setBAC(VerificationStatus.Verdict.NOT_CHECKED, ReasonCode.INSUFFICIENT_CREDENTIALS, EMPTY_TRIED_BAC_ENTRY_LIST);
298 | }
299 |
300 | /* If we have to do BAC, try to do BAC. */
301 | shouldDoBAC = featureStatus.hasBAC() == FeatureStatus.Verdict.PRESENT;
302 | }
303 |
304 | if (shouldDoBAC) {
305 | BACKeySpec bacKeySpec = tryToDoBAC(service, bacStore);
306 | if (featureStatus.hasBAC() == FeatureStatus.Verdict.UNKNOWN) {
307 | /* For some reason our test did not result in setting BAC, still apparently BAC is required. */
308 | featureStatus.setBAC(FeatureStatus.Verdict.PRESENT);
309 | }
310 | documentNumber = bacKeySpec.getDocumentNumber();
311 | }
312 | }
313 | this.lds = new LDS();
314 |
315 | /* Pre-read these files that are always present. */
316 | COMFile comFile = null;
317 | SODFile sodFile = null;
318 | DG1File dg1File = null;
319 | Collection dgNumbersAlreadyRead = new TreeSet();
320 |
321 | try {
322 | CardFileInputStream comIn = service.getInputStream(PassportService.EF_COM);
323 | lds.add(PassportService.EF_COM, comIn, comIn.getLength());
324 | comFile = lds.getCOMFile();
325 |
326 | CardFileInputStream sodIn = service.getInputStream(PassportService.EF_SOD);
327 | lds.add(PassportService.EF_SOD, sodIn, sodIn.getLength());
328 | sodFile = lds.getSODFile();
329 |
330 | CardFileInputStream dg1In = service.getInputStream(PassportService.EF_DG1);
331 | lds.add(PassportService.EF_DG1, dg1In, dg1In.getLength());
332 | dg1File = lds.getDG1File();
333 | dgNumbersAlreadyRead.add(1);
334 | if (documentNumber == null) { documentNumber = dg1File.getMRZInfo().getDocumentNumber(); }
335 | } catch (IOException ioe) {
336 | ioe.printStackTrace();
337 | LOGGER.warning("Could not read file");
338 | }
339 |
340 | if (sodFile != null) {
341 | // verifyDS(); // DEBUG 2.0.4 too costly to do this on APDU thread?!?!
342 | // verifyCS();
343 | }
344 |
345 | /* Get the list of DGs from EF.SOd, we don't trust EF.COM. */
346 | List dgNumbers = new ArrayList();
347 | if (sodFile != null) {
348 | dgNumbers.addAll(sodFile.getDataGroupHashes().keySet());
349 | } else if (comFile != null) {
350 | /* Get the list from EF.COM since we failed to parse EF.SOd. */
351 | LOGGER.warning("Failed to get DG list from EF.SOd. Getting DG list from EF.COM.");
352 | int[] tagList = comFile.getTagList();
353 | dgNumbers.addAll(toDataGroupList(tagList));
354 | }
355 | Collections.sort(dgNumbers); /* NOTE: need to sort it, since we get keys as a set. */
356 |
357 | LOGGER.info("Found DGs: " + dgNumbers);
358 |
359 | Map hashResults = verificationStatus.getHashResults();
360 | if (hashResults == null) {
361 | hashResults = new TreeMap();
362 | }
363 |
364 | if (sodFile != null) {
365 | /* Initial hash results: we know the stored hashes, but not the computed hashes yet. */
366 | Map storedHashes = sodFile.getDataGroupHashes();
367 | for (int dgNumber: dgNumbers) {
368 | byte[] storedHash = storedHashes.get(dgNumber);
369 | VerificationStatus.HashMatchResult hashResult = hashResults.get(dgNumber);
370 | if (hashResult != null) { continue; }
371 | if (dgNumbersAlreadyRead.contains(dgNumber)) {
372 | hashResult = verifyHash(dgNumber);
373 | } else {
374 | hashResult = new HashMatchResult(storedHash, null);
375 | }
376 | hashResults.put(dgNumber, hashResult);
377 | }
378 | }
379 | verificationStatus.setHT(VerificationStatus.Verdict.UNKNOWN, verificationStatus.getHTReason(), hashResults);
380 |
381 | /* Check EAC support by DG14 presence. */
382 | if (dgNumbers.contains(14)) {
383 | featureStatus.setEAC(FeatureStatus.Verdict.PRESENT);
384 | } else {
385 | featureStatus.setEAC(FeatureStatus.Verdict.NOT_PRESENT);
386 | }
387 | boolean hasEAC = featureStatus.hasEAC() == FeatureStatus.Verdict.PRESENT;
388 | List cvcaKeyStores = trustManager.getCVCAStores();
389 | if (hasEAC && cvcaKeyStores != null && cvcaKeyStores.size() > 0) {
390 | tryToDoEAC(service, lds, documentNumber, cvcaKeyStores);
391 | dgNumbersAlreadyRead.add(14);
392 | }
393 |
394 | /* Check AA support by DG15 presence. */
395 | if (dgNumbers.contains(15)) {
396 | featureStatus.setAA(FeatureStatus.Verdict.PRESENT);
397 | } else {
398 | featureStatus.setAA(FeatureStatus.Verdict.NOT_PRESENT);
399 | }
400 | boolean hasAA = featureStatus.hasAA() == FeatureStatus.Verdict.PRESENT;
401 | if (hasAA) {
402 | try {
403 | CardFileInputStream dg15In = service.getInputStream(PassportService.EF_DG15);
404 | lds.add(PassportService.EF_DG15, dg15In, dg15In.getLength());
405 | DG15File dg15File = lds.getDG15File();
406 | dgNumbersAlreadyRead.add(15);
407 | } catch (IOException ioe) {
408 | ioe.printStackTrace();
409 | LOGGER.warning("Could not read file");
410 | } catch (Exception e) {
411 | verificationStatus.setAA(VerificationStatus.Verdict.NOT_CHECKED, ReasonCode.READ_ERROR_DG15_FAILURE, null);
412 | }
413 | } else {
414 | /* Feature status says: no AA, so verification status should say: no AA. */
415 | verificationStatus.setAA(VerificationStatus.Verdict.NOT_PRESENT, ReasonCode.NOT_SUPPORTED, null);
416 | }
417 |
418 | /* Add remaining datagroups to LDS. */
419 | for (int dgNumber: dgNumbers) {
420 | if (dgNumbersAlreadyRead.contains(dgNumber)) { continue; }
421 | if ((dgNumber == 3 || dgNumber == 4) && !verificationStatus.getEAC().equals(VerificationStatus.Verdict.SUCCEEDED)) { continue; }
422 | try {
423 | short fid = LDSFileUtil.lookupFIDByDataGroupNumber(dgNumber);
424 | CardFileInputStream cardFileInputStream = service.getInputStream(fid);
425 | lds.add(fid, cardFileInputStream, cardFileInputStream.getLength());
426 | } catch (IOException ioe) {
427 | LOGGER.warning("Error reading DG" + dgNumber + ": " + ioe.getMessage());
428 | break; /* out of for loop */
429 | } catch(CardServiceException ex) {
430 | /* NOTE: Most likely EAC protected file. So log, ignore, continue with next file. */
431 | LOGGER.info("Could not read DG" + dgNumber + ": " + ex.getMessage());
432 | } catch (NumberFormatException nfe) {
433 | LOGGER.warning("NumberFormatException trying to get FID for DG" + dgNumber);
434 | nfe.printStackTrace();
435 | }
436 | }
437 | }
438 |
439 | /**
440 | * Inserts a file into this document, and updates EF_COM and EF_SOd accordingly.
441 | *
442 | * @param fid the FID of the new file
443 | * @param bytes the contents of the new file
444 | */
445 | public void putFile(short fid, byte[] bytes) {
446 | if (bytes == null) { return; }
447 | try {
448 | lds.add(fid, new ByteArrayInputStream(bytes), bytes.length);
449 | // FIXME: is this necessary?
450 | if(fid != PassportService.EF_COM && fid != PassportService.EF_SOD && fid != cvcaFID) {
451 | updateCOMSODFile(null);
452 | }
453 | } catch (IOException ioe) {
454 | ioe.printStackTrace();
455 | }
456 | verificationStatus.setAll(VerificationStatus.Verdict.UNKNOWN, ReasonCode.UNKNOWN); // FIXME: why all?
457 | }
458 |
459 | /**
460 | * Updates EF_COM and EF_SOd using a new document signing certificate.
461 | *
462 | * @param newCertificate a certificate
463 | */
464 | public void updateCOMSODFile(X509Certificate newCertificate) {
465 | try {
466 | COMFile comFile = lds.getCOMFile();
467 | SODFile sodFile = lds.getSODFile();
468 | String digestAlg = sodFile.getDigestAlgorithm();
469 | String signatureAlg = sodFile.getDigestEncryptionAlgorithm();
470 | X509Certificate cert = newCertificate != null ? newCertificate : sodFile.getDocSigningCertificate();
471 | byte[] signature = sodFile.getEncryptedDigest();
472 | Map dgHashes = new TreeMap();
473 | List dgFids = lds.getDataGroupList();
474 | MessageDigest digest = null;
475 | digest = MessageDigest.getInstance(digestAlg);
476 | for (Short fid : dgFids) {
477 | if (fid != PassportService.EF_COM && fid != PassportService.EF_SOD && fid != cvcaFID) {
478 | int length = lds.getLength(fid);
479 | InputStream inputStream = lds.getInputStream(fid);
480 | if (inputStream == null) { LOGGER.warning("Could not get input stream for " + Integer.toHexString(fid)); continue; }
481 | DataInputStream dataInputStream = new DataInputStream(inputStream);
482 | byte[] data = new byte[length];
483 | dataInputStream.readFully(data);
484 | byte tag = data[0];
485 | dgHashes.put(LDSFileUtil.lookupDataGroupNumberByTag(tag), digest.digest(data));
486 | comFile.insertTag((int)(tag & 0xFF));
487 | }
488 | }
489 | if(docSigningPrivateKey != null) {
490 | sodFile = new SODFile(digestAlg, signatureAlg, dgHashes, docSigningPrivateKey, cert);
491 | } else {
492 | sodFile = new SODFile(digestAlg, signatureAlg, dgHashes, signature, cert);
493 | }
494 | lds.add(comFile);
495 | lds.add(sodFile);
496 | } catch (Exception e) {
497 | e.printStackTrace();
498 | }
499 | }
500 |
501 | public LDS getLDS() {
502 | return lds;
503 | }
504 |
505 | /**
506 | * Sets the document signing private key.
507 | *
508 | * @param docSigningPrivateKey a private key
509 | */
510 | public void setDocSigningPrivateKey(PrivateKey docSigningPrivateKey) {
511 | this.docSigningPrivateKey = docSigningPrivateKey;
512 | updateCOMSODFile(null);
513 | }
514 |
515 | /**
516 | * Gets the CVCA certificate.
517 | *
518 | * @return a CV certificate or null
519 | */
520 | public CardVerifiableCertificate getCVCertificate() {
521 | return cvcaCertificate;
522 | }
523 |
524 | /**
525 | * Sets the CVCA certificate.
526 | *
527 | * @param cert the CV certificate
528 | */
529 | public void setCVCertificate(CardVerifiableCertificate cert) {
530 | this.cvcaCertificate = cert;
531 | try {
532 | CVCAFile cvcaFile = new CVCAFile(cvcaFID, cvcaCertificate.getHolderReference().getName());
533 | putFile(cvcaFID, cvcaFile.getEncoded());
534 | } catch (CertificateException ce) {
535 | ce.printStackTrace();
536 | }
537 | }
538 |
539 | /**
540 | * Gets the document signing private key, or null
if not present.
541 | *
542 | * @return a private key or null
543 | */
544 | public PrivateKey getDocSigningPrivateKey() {
545 | return docSigningPrivateKey;
546 | }
547 |
548 | /**
549 | * Sets the document signing certificate.
550 | *
551 | * @param docSigningCertificate a certificate
552 | */
553 | public void setDocSigningCertificate(X509Certificate docSigningCertificate) {
554 | updateCOMSODFile(docSigningCertificate);
555 | }
556 |
557 | /**
558 | * Gets the CSCA, CVCA trust store.
559 | *
560 | * @return the trust store in use
561 | */
562 | public MRTDTrustStore getTrustManager() {
563 | return trustManager;
564 | }
565 |
566 | /**
567 | * Gets the private key for EAC, or null
if not present.
568 | *
569 | * @return a private key or null
570 | */
571 | public PrivateKey getEACPrivateKey() {
572 | return eacPrivateKey;
573 | }
574 |
575 | /**
576 | * Sets the private key for EAC.
577 | *
578 | * @param eacPrivateKey a private key
579 | */
580 | public void setEACPrivateKey(PrivateKey eacPrivateKey) {
581 | this.eacPrivateKey = eacPrivateKey;
582 | }
583 |
584 | /**
585 | * Sets the public key for EAC.
586 | *
587 | * @param eacPublicKey a public key
588 | */
589 | public void setEACPublicKey(PublicKey eacPublicKey) {
590 | ChipAuthenticationPublicKeyInfo chipAuthenticationPublicKeyInfo = new ChipAuthenticationPublicKeyInfo(eacPublicKey);
591 | DG14File dg14File = new DG14File(Arrays.asList(new SecurityInfo[] { chipAuthenticationPublicKeyInfo }));
592 | putFile(PassportService.EF_DG14, dg14File.getEncoded());
593 | }
594 |
595 | /**
596 | * Gets the private key for AA, or null
if not present.
597 | *
598 | * @return a private key or null
599 | */
600 | public PrivateKey getAAPrivateKey() {
601 | return aaPrivateKey;
602 | }
603 |
604 | /**
605 | * Sets the private key for AA.
606 | *
607 | * @param aaPrivateKey a private key
608 | */
609 | public void setAAPrivateKey(PrivateKey aaPrivateKey) {
610 | this.aaPrivateKey = aaPrivateKey;
611 | }
612 |
613 | /**
614 | * Sets the public key for AA.
615 | *
616 | * @param aaPublicKey a public key
617 | */
618 | public void setAAPublicKey(PublicKey aaPublicKey) {
619 | DG15File dg15file = new DG15File(aaPublicKey);
620 | putFile(PassportService.EF_DG15, dg15file.getEncoded());
621 | }
622 |
623 | /**
624 | * Gets the supported features (such as: BAC, AA, EAC) as
625 | * discovered during initialization of this document.
626 | *
627 | * @return the supported features
628 | *
629 | * @since 0.4.9
630 | */
631 | public FeatureStatus getFeatures() {
632 | /* The feature status has been created in constructor. */
633 | return featureStatus;
634 | }
635 |
636 | /**
637 | * Gets the verification status thus far.
638 | *
639 | * @return the verification status
640 | *
641 | * @since 0.4.9
642 | */
643 | public VerificationStatus getVerificationStatus() {
644 | return verificationStatus;
645 | }
646 |
647 | /* ONLY PRIVATE METHODS BELOW. */
648 |
649 | private BACKeySpec tryToDoBAC(PassportService service, List bacStore) throws BACDeniedException {
650 | List triedBACEntries = new ArrayList();
651 | int lastKnownSW = BACDeniedException.SW_NONE;
652 |
653 | synchronized (bacStore) {
654 | for (BACKeySpec bacKey: bacStore) {
655 | try {
656 | triedBACEntries.add(bacKey);
657 | tryToDoBAC(service, bacKey);
658 | verificationStatus.setBAC(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.SUCCEEDED, triedBACEntries);
659 | return bacKey;
660 | } catch (CardServiceException cse) {
661 | LOGGER.info("Ignoring the following exception: " + cse.getClass().getCanonicalName());
662 | cse.printStackTrace(); // DEBUG: this line was commented in production
663 | lastKnownSW = cse.getSW();
664 | /* NOTE: BAC failed? Try next BACEntry */
665 | }
666 | }
667 | }
668 |
669 | /* Document requires BAC, but we failed to authenticate. */
670 | verificationStatus.setBAC(VerificationStatus.Verdict.FAILED, ReasonCode.INSUFFICIENT_CREDENTIALS, triedBACEntries);
671 | throw new BACDeniedException("Basic Access denied!", triedBACEntries, lastKnownSW);
672 | }
673 |
674 | private void tryToDoBAC(PassportService service, BACKeySpec bacKey) throws CardServiceException {
675 | try {
676 | LOGGER.info("Trying BAC: " + bacKey);
677 | service.doBAC(bacKey);
678 | /* NOTE: if successful, doBAC te catch (CardServiceException cse) {
679 | e.thrrminates normally, otherwise exception. */
680 | } catch (Exception e) {
681 | if (e instanceof CardServiceException) { throw (CardServiceException)e; }
682 | LOGGER.warning("DEBUG: Unexpected exception " + e.getClass().getCanonicalName() + " during BAC with " + bacKey);
683 | e.printStackTrace();
684 | throw new CardServiceException(e.getMessage());
685 | }
686 | }
687 |
688 | private boolean tryToDoPACE(PassportService service, PACEInfo paceInfo, BACKeySpec bacKey) throws CardServiceException {
689 | // LOGGER.info("DEBUG: PACE has been disabled in this version of JMRTD");
690 | // return false;
691 |
692 | LOGGER.info("DEBUG: attempting doPACE with PACEInfo " + paceInfo);
693 | service.doPACE(bacKey, paceInfo.getObjectIdentifier(), PACEInfo.toParameterSpec(paceInfo.getParameterId()));
694 | return true;
695 | }
696 |
697 | private void tryToDoEAC(PassportService service, LDS lds, String documentNumber, List cvcaKeyStores) throws CardServiceException {
698 | DG14File dg14File = null;
699 | CVCAFile cvcaFile = null;
700 |
701 | try {
702 | try {
703 | /* Make sure DG14 is read. */
704 | CardFileInputStream dg14In = service.getInputStream(PassportService.EF_DG14);
705 | lds.add(PassportService.EF_DG14, dg14In, dg14In.getLength());
706 | dg14File = lds.getDG14File();
707 |
708 | /* Now try to deal with EF.CVCA. */
709 | cvcaFID = PassportService.EF_CVCA; /* Default CVCA file Id */
710 | List cvcaFIDs = dg14File.getCVCAFileIds();
711 | if (cvcaFIDs != null && cvcaFIDs.size() != 0) {
712 | if (cvcaFIDs.size() > 1) { LOGGER.warning("More than one CVCA file id present in DG14"); }
713 | cvcaFID = cvcaFIDs.get(0).shortValue(); /* Possibly different from default. */
714 | }
715 | CardFileInputStream cvcaIn = service.getInputStream(cvcaFID);
716 | lds.add(cvcaFID, cvcaIn, cvcaIn.getLength());
717 | cvcaFile = lds.getCVCAFile();
718 | } catch (IOException ioe) {
719 | ioe.printStackTrace();
720 | LOGGER.warning("Could not read EF.DG14 or EF.CVCA, not attempting EAC");
721 | return;
722 | }
723 |
724 | /* Try to do EAC. */
725 | CVCPrincipal[] possibleCVCAReferences = new CVCPrincipal[]{ cvcaFile.getCAReference(), cvcaFile.getAltCAReference() };
726 | for (CVCPrincipal caReference: possibleCVCAReferences) {
727 | EACCredentials eacCredentials = getEACCredentials(caReference, cvcaKeyStores);
728 | if (eacCredentials == null) { continue; }
729 |
730 | PrivateKey privateKey = eacCredentials.getPrivateKey();
731 | Certificate[] chain = eacCredentials.getChain();
732 | List terminalCerts = new ArrayList(chain.length);
733 | for (Certificate c: chain) { terminalCerts.add((CardVerifiableCertificate)c); }
734 |
735 | Map cardKeys = dg14File.getChipAuthenticationPublicKeyInfos();
736 | for (Map.Entry entry: cardKeys.entrySet()) {
737 | BigInteger keyId = entry.getKey();
738 | PublicKey publicKey = entry.getValue();
739 | try {
740 | ChipAuthenticationResult chipAuthenticationResult = service.doCA(keyId, publicKey);
741 | TerminalAuthenticationResult eacResult = service.doTA(caReference, terminalCerts, privateKey, null, chipAuthenticationResult, documentNumber);
742 | verificationStatus.setEAC(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.SUCCEEDED, eacResult);
743 | } catch(CardServiceException cse) {
744 | cse.printStackTrace();
745 | /* NOTE: Failed? Too bad, try next public key. */
746 | continue;
747 | }
748 | }
749 |
750 | break;
751 | }
752 | } catch (Exception e) {
753 | LOGGER.warning("EAC failed with exception " + e.getMessage());
754 | e.printStackTrace();
755 | }
756 | }
757 |
758 | /**
759 | * Encapsulates the terminal key and associated certificte chain for terminal authentication.
760 | */
761 | class EACCredentials {
762 | private PrivateKey privateKey;
763 | private Certificate[] chain;
764 |
765 | /**
766 | * Creates EAC credentials.
767 | *
768 | * @param privateKey
769 | * @param chain
770 | */
771 | public EACCredentials(PrivateKey privateKey, Certificate[] chain) {
772 | this.privateKey = privateKey;
773 | this.chain = chain;
774 | }
775 |
776 | public PrivateKey getPrivateKey() {
777 | return privateKey;
778 | }
779 |
780 | public Certificate[] getChain() {
781 | return chain;
782 | }
783 | }
784 |
785 | private EACCredentials getEACCredentials(CVCPrincipal caReference, List cvcaStores) throws GeneralSecurityException {
786 | for (KeyStore cvcaStore: cvcaStores) {
787 | EACCredentials eacCredentials = getEACCredentials(caReference, cvcaStore);
788 | if (eacCredentials != null) { return eacCredentials; }
789 | }
790 | return null;
791 | }
792 |
793 | /**
794 | * Searches the key store for a relevant terminal key and associated certificate chain.
795 | *
796 | * @param caReference
797 | * @param cvcaStore should contain a single key with certificate chain
798 | * @return
799 | * @throws GeneralSecurityException
800 | */
801 | private EACCredentials getEACCredentials(CVCPrincipal caReference, KeyStore cvcaStore) throws GeneralSecurityException {
802 | if (caReference == null) { throw new IllegalArgumentException("CA reference cannot be null"); }
803 |
804 | PrivateKey privateKey = null;
805 | Certificate[] chain = null;
806 |
807 | List aliases = Collections.list(cvcaStore.aliases());
808 | for (String alias: aliases) {
809 | if (cvcaStore.isKeyEntry(alias)) {
810 | Security.insertProviderAt(BC_PROVIDER, 0);
811 | Key key = cvcaStore.getKey(alias, "".toCharArray());
812 | if (key instanceof PrivateKey) {
813 | privateKey = (PrivateKey)key;
814 | } else {
815 | LOGGER.warning("skipping non-private key " + alias);
816 | continue;
817 | }
818 | chain = cvcaStore.getCertificateChain(alias);
819 | return new EACCredentials(privateKey, chain);
820 | } else if (cvcaStore.isCertificateEntry(alias)) {
821 | CardVerifiableCertificate certificate = (CardVerifiableCertificate)cvcaStore.getCertificate(alias);
822 | CVCPrincipal authRef = certificate.getAuthorityReference();
823 | CVCPrincipal holderRef = certificate.getHolderReference();
824 | if (!caReference.equals(authRef)) { continue; }
825 | /* See if we have a private key for that certificate. */
826 | privateKey = (PrivateKey)cvcaStore.getKey(holderRef.getName(), "".toCharArray());
827 | chain = cvcaStore.getCertificateChain(holderRef.getName());
828 | if (privateKey == null) { continue; }
829 | LOGGER.fine("found a key, privateKey = " + privateKey);
830 | return new EACCredentials(privateKey, chain);
831 | }
832 | if (privateKey == null || chain == null) {
833 | LOGGER.severe("null chain or key for entry " + alias + ": chain = " + Arrays.toString(chain) + ", privateKey = " + privateKey);
834 | continue;
835 | }
836 | }
837 | return null;
838 | }
839 |
840 | /**
841 | * Builds a certificate chain to an anchor using the PKIX algorithm.
842 | *
843 | * @param docSigningCertificate the start certificate
844 | * @param sodIssuer the issuer of the start certificate (ignored unless docSigningCertificate
is null
)
845 | * @param sodSerialNumber the serial number of the start certificate (ignored unless docSigningCertificate
is null
)
846 | *
847 | * @return the certificate chain
848 | */
849 | private static List getCertificateChain(X509Certificate docSigningCertificate,
850 | final X500Principal sodIssuer, final BigInteger sodSerialNumber,
851 | List cscaStores, Set cscaTrustAnchors) {
852 | List chain = new ArrayList();
853 | X509CertSelector selector = new X509CertSelector();
854 | try {
855 |
856 | if (docSigningCertificate != null) {
857 | selector.setCertificate(docSigningCertificate);
858 | } else {
859 | selector.setIssuer(sodIssuer);
860 | selector.setSerialNumber(sodSerialNumber);
861 | }
862 |
863 | CertStoreParameters docStoreParams = new CollectionCertStoreParameters(Collections.singleton((Certificate)docSigningCertificate));
864 | CertStore docStore = CertStore.getInstance("Collection", docStoreParams);
865 |
866 | CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", BC_PROVIDER);
867 | PKIXBuilderParameters buildParams = new PKIXBuilderParameters(cscaTrustAnchors, selector);
868 | buildParams.addCertStore(docStore);
869 | for (CertStore trustStore: cscaStores) {
870 | buildParams.addCertStore(trustStore);
871 | }
872 | buildParams.setRevocationEnabled(IS_PKIX_REVOCATION_CHECING_ENABLED); /* NOTE: set to false for checking disabled. */
873 | Security.addProvider(BC_PROVIDER); /* DEBUG: needed, or builder will throw a runtime exception. FIXME! */
874 | PKIXCertPathBuilderResult result = null;
875 |
876 | try {
877 | result = (PKIXCertPathBuilderResult)builder.build(buildParams);
878 | } catch (CertPathBuilderException cpbe) {
879 | /* NOTE: ignore, result remain null */
880 | }
881 | if (result != null) {
882 | CertPath pkixCertPath = result.getCertPath();
883 | if (pkixCertPath != null) {
884 | chain.addAll(pkixCertPath.getCertificates());
885 | }
886 | }
887 | if (docSigningCertificate != null && !chain.contains(docSigningCertificate)) {
888 | /* NOTE: if doc signing certificate not in list, we add it ourselves. */
889 | LOGGER.warning("Adding doc signing certificate after PKIXBuilder finished");
890 | chain.add(0, docSigningCertificate);
891 | }
892 | if (result != null) {
893 | Certificate trustAnchorCertificate = result.getTrustAnchor().getTrustedCert();
894 | if (trustAnchorCertificate != null && !chain.contains(trustAnchorCertificate)) {
895 | /* NOTE: if trust anchor not in list, we add it ourselves. */
896 | LOGGER.warning("Adding trust anchor certificate after PKIXBuilder finished");
897 | chain.add(trustAnchorCertificate);
898 | }
899 | }
900 | } catch (Exception e) {
901 | e.printStackTrace();
902 | LOGGER.info("Building a chain failed (" + e.getMessage() + ").");
903 | }
904 | return chain;
905 | }
906 |
907 | /**
908 | * Check active authentication.
909 | */
910 | public void verifyAA() {
911 | int challengeLength = 8;
912 | byte[] challenge = new byte[challengeLength];
913 | random.nextBytes(challenge);
914 | ActiveAuthenticationResult aaResult = executeAA(challenge);
915 | verifyAA(aaResult);
916 | }
917 |
918 | /**
919 | * Execute active authentication using the given challenge.
920 | *
921 | * @param challenge an byte array of length 8
922 | */
923 | public ActiveAuthenticationResult executeAA(byte[] challenge) {
924 | if (lds == null || service == null) {
925 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.UNKNOWN, null);
926 | return null;
927 | }
928 |
929 | try {
930 | DG15File dg15File = lds.getDG15File();
931 | if (dg15File == null) {
932 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.READ_ERROR_DG15_FAILURE, null);
933 | return null;
934 | }
935 | PublicKey pubKey = dg15File.getPublicKey();
936 | String pubKeyAlgorithm = pubKey.getAlgorithm();
937 | String digestAlgorithm = "SHA1";
938 | String signatureAlgorithm = "SHA1WithRSA/ISO9796-2";
939 | if ("EC".equals(pubKeyAlgorithm) || "ECDSA".equals(pubKeyAlgorithm)) {
940 | DG14File dg14File = lds.getDG14File();
941 | if (dg14File == null) {
942 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.READ_ERROR_DG14_FAILURE, null);
943 | return null;
944 | }
945 | List activeAuthenticationInfos = dg14File.getActiveAuthenticationInfos();
946 | int activeAuthenticationInfoCount = (activeAuthenticationInfos == null ? 0 : activeAuthenticationInfos.size());
947 | if (activeAuthenticationInfoCount < 1) {
948 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.READ_ERROR_DG14_FAILURE, null);
949 | return null;
950 | } else if (activeAuthenticationInfoCount > 1) {
951 | LOGGER.warning("Found " + activeAuthenticationInfoCount + " in EF.DG14, expected 1.");
952 | }
953 | ActiveAuthenticationInfo activeAuthenticationInfo = activeAuthenticationInfos.get(0);
954 | String signatureAlgorithmOID = activeAuthenticationInfo.getSignatureAlgorithmOID();
955 | signatureAlgorithm = ActiveAuthenticationInfo.lookupMnemonicByOID(signatureAlgorithmOID);
956 | digestAlgorithm = Util.inferDigestAlgorithmFromSignatureAlgorithm(signatureAlgorithm);
957 | }
958 | byte[] response = service.doAA(pubKey, digestAlgorithm, signatureAlgorithm, challenge);
959 | return new ActiveAuthenticationResult(pubKey, digestAlgorithm, signatureAlgorithm, challenge, response);
960 | } catch (CardServiceException cse) {
961 | cse.printStackTrace();
962 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE, null);
963 | return null;
964 | } catch (Exception e) {
965 | LOGGER.severe("DEBUG: this exception wasn't caught in verification logic (< 0.4.8) -- MO 3. Type is " + e.getClass().getCanonicalName());
966 | e.printStackTrace();
967 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE, null);
968 | return null;
969 | }
970 | }
971 |
972 | /**
973 | * Check the active authentication result.
974 | *
975 | * @param aaResult
976 | * @return
977 | */
978 | public boolean verifyAA(ActiveAuthenticationResult aaResult) {
979 | try {
980 | PublicKey publicKey = aaResult.getPublicKey();
981 | String digestAlgorithm = aaResult.getDigestAlgorithm();
982 | String signatureAlgorithm = aaResult.getSignatureAlgorithm();
983 | byte[] challenge = aaResult.getChallenge();
984 | byte[] response = aaResult.getResponse();
985 |
986 | String pubKeyAlgorithm = publicKey.getAlgorithm();
987 | if ("RSA".equals(pubKeyAlgorithm)) {
988 | /* FIXME: check that digestAlgorithm = "SHA1" in this case, check (and re-initialize) rsaAASignature (and rsaAACipher). */
989 | if (!"SHA1".equalsIgnoreCase(digestAlgorithm)
990 | || !"SHA-1".equalsIgnoreCase(digestAlgorithm)
991 | || !"SHA1WithRSA/ISO9796-2".equalsIgnoreCase(signatureAlgorithm)) {
992 | LOGGER.warning("Unexpected algorithms for RSA AA: "
993 | + "digest algorithm = " + (digestAlgorithm == null ? "null" : digestAlgorithm)
994 | + ", signature algorithm = " + (signatureAlgorithm == null ? "null" : signatureAlgorithm));
995 |
996 | rsaAADigest = MessageDigest.getInstance(digestAlgorithm); /* NOTE: for output length measurement only. -- MO */
997 | rsaAASignature = Signature.getInstance(signatureAlgorithm, BC_PROVIDER);
998 | }
999 |
1000 | RSAPublicKey rsaPublicKey = (RSAPublicKey)publicKey;
1001 | rsaAACipher.init(Cipher.DECRYPT_MODE, rsaPublicKey);
1002 | rsaAASignature.initVerify(rsaPublicKey);
1003 |
1004 | int digestLength = rsaAADigest.getDigestLength(); /* SHA1 should be 20 bytes = 160 bits */
1005 | assert(digestLength == 20);
1006 | byte[] plaintext = rsaAACipher.doFinal(response);
1007 | byte[] m1 = Util.recoverMessage(digestLength, plaintext);
1008 | rsaAASignature.update(m1);
1009 | rsaAASignature.update(challenge);
1010 | boolean success = rsaAASignature.verify(response);
1011 |
1012 | if (success) {
1013 | verificationStatus.setAA(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.SIGNATURE_CHECKED, aaResult);
1014 | } else {
1015 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.SIGNATURE_FAILURE, aaResult);
1016 | }
1017 | return success;
1018 | } else if ("EC".equals(pubKeyAlgorithm) || "ECDSA".equals(pubKeyAlgorithm)) {
1019 | ECPublicKey ecdsaPublicKey = (ECPublicKey)publicKey;
1020 |
1021 | if (ecdsaAASignature == null || signatureAlgorithm != null && !signatureAlgorithm.equals(ecdsaAASignature.getAlgorithm())) {
1022 | LOGGER.warning("Re-initializing ecdsaAASignature with signature algorithm " + signatureAlgorithm);
1023 | ecdsaAASignature = Signature.getInstance(signatureAlgorithm);
1024 | }
1025 | if (ecdsaAADigest == null || digestAlgorithm != null && !digestAlgorithm.equals(ecdsaAADigest.getAlgorithm())) {
1026 | LOGGER.warning("Re-initializing ecdsaAADigest with digest algorithm " + digestAlgorithm);
1027 | ecdsaAADigest = MessageDigest.getInstance(digestAlgorithm);
1028 | }
1029 |
1030 | ecdsaAASignature.initVerify(ecdsaPublicKey);
1031 |
1032 | if (response.length % 2 != 0) {
1033 | LOGGER.warning("Active Authentication response is not of even length");
1034 | }
1035 |
1036 | int l = response.length / 2;
1037 | BigInteger r = Util.os2i(response, 0, l);
1038 | BigInteger s = Util.os2i(response, l, l);
1039 |
1040 | ecdsaAASignature.update(challenge);
1041 |
1042 | try {
1043 |
1044 | ASN1Sequence asn1Sequence = new DERSequence(new ASN1Encodable[] { new ASN1Integer(r), new ASN1Integer(s) });
1045 | boolean success = ecdsaAASignature.verify(asn1Sequence.getEncoded());
1046 | if (success) {
1047 | verificationStatus.setAA(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.SUCCEEDED, aaResult);
1048 | } else {
1049 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.SIGNATURE_FAILURE, aaResult);
1050 | }
1051 | return success;
1052 | } catch (IOException ioe) {
1053 | LOGGER.severe("Unexpected exception during AA signature verification with ECDSA");
1054 | ioe.printStackTrace();
1055 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE, aaResult);
1056 | return false;
1057 | }
1058 | } else {
1059 | LOGGER.severe("Unsupported AA public key type " + publicKey.getClass().getSimpleName());
1060 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.UNSUPPORTED_KEY_TYPE_FAILURE, aaResult);
1061 | return false;
1062 | }
1063 | } catch (Exception e) {
1064 | verificationStatus.setAA(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE, aaResult);
1065 | return false;
1066 | }
1067 | }
1068 |
1069 | /**
1070 | * Checks the security object's signature.
1071 | *
1072 | * TODO: Check the cert stores (notably PKD) to fetch document signer certificate (if not embedded in SOd) and check its validity before checking the signature.
1073 | */
1074 | public void verifyDS() {
1075 | try {
1076 | verificationStatus.setDS(VerificationStatus.Verdict.UNKNOWN, ReasonCode.UNKNOWN);
1077 |
1078 | SODFile sod = lds.getSODFile();
1079 |
1080 | /* Check document signing signature. */
1081 | X509Certificate docSigningCert = sod.getDocSigningCertificate();
1082 | if (docSigningCert == null) {
1083 | LOGGER.warning("Could not get document signer certificate from EF.SOd");
1084 | // FIXME: We search for it in cert stores. See note at verifyCS.
1085 | // X500Principal issuer = sod.getIssuerX500Principal();
1086 | // BigInteger serialNumber = sod.getSerialNumber();
1087 | }
1088 | if (sod.checkDocSignature(docSigningCert)) {
1089 | verificationStatus.setDS(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.SIGNATURE_CHECKED);
1090 | } else {
1091 | verificationStatus.setDS(VerificationStatus.Verdict.FAILED, ReasonCode.SIGNATURE_FAILURE);
1092 | }
1093 | } catch (NoSuchAlgorithmException nsae) {
1094 | verificationStatus.setDS(VerificationStatus.Verdict.FAILED, ReasonCode.UNSUPPORTED_SIGNATURE_ALGORITHM_FAILURE);
1095 | return; /* NOTE: Serious enough to not perform other checks, leave method. */
1096 | } catch (Exception e) {
1097 | e.printStackTrace();
1098 | verificationStatus.setDS(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE);
1099 | return; /* NOTE: Serious enough to not perform other checks, leave method. */
1100 | }
1101 | }
1102 |
1103 | /**
1104 | * Checks the certificate chain.
1105 | */
1106 | public void verifyCS() {
1107 | try {
1108 | /* Get EF.SOd. */
1109 | SODFile sod = null;
1110 | try {
1111 | sod = lds.getSODFile();
1112 | } catch (IOException ioe) {
1113 | LOGGER.severe("Could not read EF.SOd");
1114 | }
1115 | List chain = new ArrayList();
1116 |
1117 | if (sod == null) {
1118 | verificationStatus.setCS(VerificationStatus.Verdict.FAILED, ReasonCode.COULD_NOT_BUILD_CHAIN_FAILURE, chain);
1119 | return;
1120 | }
1121 |
1122 | /* Get doc signing certificate and issuer info. */
1123 | X509Certificate docSigningCertificate = null;
1124 | X500Principal sodIssuer = null;
1125 | BigInteger sodSerialNumber = null;
1126 | try {
1127 | sodIssuer = sod.getIssuerX500Principal();
1128 | sodSerialNumber = sod.getSerialNumber();
1129 | docSigningCertificate = sod.getDocSigningCertificate();
1130 | } catch (Exception e) {
1131 | LOGGER.warning("Error getting document signing certificate: " + e.getMessage());
1132 | // FIXME: search for it in cert stores?
1133 | }
1134 |
1135 | if (docSigningCertificate != null) {
1136 | chain.add(docSigningCertificate);
1137 | } else {
1138 | LOGGER.warning("Error getting document signing certificate from EF.SOd");
1139 | }
1140 |
1141 | /* Get trust anchors. */
1142 | List cscaStores = trustManager.getCSCAStores();
1143 | if (cscaStores == null || cscaStores.size() <= 0) {
1144 | LOGGER.warning("No CSCA certificate stores found.");
1145 | verificationStatus.setCS(VerificationStatus.Verdict.FAILED, ReasonCode.NO_CSCA_TRUST_ANCHORS_FOUND_FAILURE, chain);
1146 | }
1147 | Set cscaTrustAnchors = trustManager.getCSCAAnchors();
1148 | if (cscaTrustAnchors == null || cscaTrustAnchors.size() <= 0) {
1149 | LOGGER.warning("No CSCA trust anchors found.");
1150 | verificationStatus.setCS(VerificationStatus.Verdict.FAILED, ReasonCode.NO_CSCA_TRUST_ANCHORS_FOUND_FAILURE, chain);
1151 | }
1152 |
1153 | /* Optional internal EF.SOd consistency check. */
1154 | if (docSigningCertificate != null) {
1155 | X500Principal docIssuer = docSigningCertificate.getIssuerX500Principal();
1156 | if (sodIssuer != null && !sodIssuer.equals(docIssuer)) {
1157 | LOGGER.severe("Security object issuer principal is different from embedded DS certificate issuer!");
1158 | }
1159 | BigInteger docSerialNumber = docSigningCertificate.getSerialNumber();
1160 | if (sodSerialNumber != null && !sodSerialNumber.equals(docSerialNumber)) {
1161 | LOGGER.warning("Security object serial number is different from embedded DS certificate serial number!");
1162 | }
1163 | }
1164 |
1165 | /* Run PKIX algorithm to build chain to any trust anchor. Add certificates to our chain. */
1166 | List pkixChain = getCertificateChain(docSigningCertificate, sodIssuer, sodSerialNumber, cscaStores, cscaTrustAnchors);
1167 | if (pkixChain == null) {
1168 | verificationStatus.setCS(VerificationStatus.Verdict.FAILED, ReasonCode.SIGNATURE_FAILURE, chain);
1169 | return;
1170 | }
1171 |
1172 | for (Certificate certificate: pkixChain) {
1173 | if (certificate.equals(docSigningCertificate)) { continue; } /* Ignore DS certificate, which is already in chain. */
1174 | chain.add(certificate);
1175 | }
1176 |
1177 | int chainDepth = chain.size();
1178 | if (chainDepth <= 1) {
1179 | verificationStatus.setCS(VerificationStatus.Verdict.FAILED, ReasonCode.COULD_NOT_BUILD_CHAIN_FAILURE, chain);
1180 | return;
1181 | }
1182 | if (chainDepth > 1 && verificationStatus.getCS().equals(VerificationStatus.Verdict.UNKNOWN)) {
1183 | verificationStatus.setCS(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.FOUND_A_CHAIN_SUCCEEDED, chain);
1184 | }
1185 | } catch (Exception e) {
1186 | e.printStackTrace();
1187 | verificationStatus.setCS(VerificationStatus.Verdict.FAILED, ReasonCode.SIGNATURE_FAILURE, EMPTY_CERTIFICATE_CHAIN);
1188 | }
1189 | }
1190 |
1191 | /**
1192 | * Checks hashes in the SOd correspond to hashes we compute.
1193 | */
1194 | public void verifyHT() {
1195 | /* Compare stored hashes to computed hashes. */
1196 | Map hashResults = verificationStatus.getHashResults();
1197 | if (hashResults == null) {
1198 | hashResults = new TreeMap();
1199 | }
1200 |
1201 | SODFile sod = null;
1202 | try {
1203 | sod = lds.getSODFile();
1204 | } catch (Exception e) {
1205 | verificationStatus.setHT(VerificationStatus.Verdict.FAILED, ReasonCode.READ_ERROR_SOD_FAILURE, hashResults);
1206 | return;
1207 | }
1208 | Map storedHashes = sod.getDataGroupHashes();
1209 | for (int dgNumber: storedHashes.keySet()) {
1210 | verifyHash(dgNumber, hashResults);
1211 | }
1212 | if (verificationStatus.getHT().equals(VerificationStatus.Verdict.UNKNOWN)) {
1213 | verificationStatus.setHT(VerificationStatus.Verdict.SUCCEEDED, ReasonCode.ALL_HASHES_MATCH, hashResults);
1214 | } else {
1215 | /* Update storedHashes and computedHashes. */
1216 | verificationStatus.setHT(verificationStatus.getHT(), verificationStatus.getHTReason(), hashResults);
1217 | }
1218 | }
1219 |
1220 | private HashMatchResult verifyHash(int dgNumber) {
1221 | Map hashResults = verificationStatus.getHashResults();
1222 | if (hashResults == null) {
1223 | hashResults = new TreeMap();
1224 | }
1225 | return verifyHash(dgNumber, hashResults);
1226 | }
1227 |
1228 | /**
1229 | * Verifies the hash for the given datagroup.
1230 | * Note that this will block until all bytes of the datagroup
1231 | * are loaded.
1232 | *
1233 | * @param dgNumber
1234 | * @param digest an existing digest that will be reused (this method will reset it)
1235 | * @param storedHash the stored hash for this datagroup
1236 | * @param hashResults the hashtable status to update
1237 | */
1238 | private VerificationStatus.HashMatchResult verifyHash(int dgNumber, Map hashResults) {
1239 | short fid = LDSFileUtil.lookupFIDByTag(LDSFileUtil.lookupTagByDataGroupNumber(dgNumber));
1240 |
1241 | SODFile sod = null;
1242 |
1243 | /* Get the stored hash for the DG. */
1244 | byte[] storedHash = null;
1245 | try {
1246 | sod = lds.getSODFile();
1247 | Map storedHashes = sod.getDataGroupHashes();
1248 | storedHash = storedHashes.get(dgNumber);
1249 | } catch(Exception e) {
1250 | verificationStatus.setHT(VerificationStatus.Verdict.FAILED, ReasonCode.STORED_HASH_NOT_FOUND_FAILURE, hashResults);
1251 | return null;
1252 | }
1253 |
1254 | /* Initialize hash. */
1255 | String digestAlgorithm = sod.getDigestAlgorithm();
1256 | try {
1257 | digest = getDigest(digestAlgorithm);
1258 | } catch (NoSuchAlgorithmException nsae) {
1259 | verificationStatus.setHT(VerificationStatus.Verdict.FAILED, ReasonCode.UNSUPPORTED_DIGEST_ALGORITHM_FAILURE, null);
1260 | return null; // DEBUG -- MO
1261 | }
1262 |
1263 | /* Read the DG. */
1264 | byte[] dgBytes = null;
1265 | try {
1266 | InputStream dgIn = null;
1267 | int length = lds.getLength(fid);
1268 | if (length > 0) {
1269 | dgBytes = new byte[length];
1270 | dgIn = lds.getInputStream(fid);
1271 | DataInputStream dgDataIn = new DataInputStream(dgIn);
1272 | dgDataIn.readFully(dgBytes);
1273 | }
1274 |
1275 | if (dgIn == null && (verificationStatus.getEAC() != VerificationStatus.Verdict.SUCCEEDED) && (fid == PassportService.EF_DG3 || fid == PassportService.EF_DG4)) {
1276 | LOGGER.warning("Skipping DG" + dgNumber + " during HT verification because EAC failed.");
1277 | VerificationStatus.HashMatchResult hashResult = new HashMatchResult(storedHash, null);
1278 | hashResults.put(dgNumber, hashResult);
1279 | return hashResult;
1280 | }
1281 | if (dgIn == null) {
1282 | LOGGER.warning("Skipping DG" + dgNumber + " during HT verification because file could not be read.");
1283 | VerificationStatus.HashMatchResult hashResult = new HashMatchResult(storedHash, null);
1284 | hashResults.put(dgNumber, hashResult);
1285 | return hashResult;
1286 | }
1287 |
1288 | } catch(Exception e) {
1289 | VerificationStatus.HashMatchResult hashResult = new HashMatchResult(storedHash, null);
1290 | hashResults.put(dgNumber, hashResult);
1291 | verificationStatus.setHT(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE, hashResults);
1292 | return hashResult;
1293 | }
1294 |
1295 | /* Compute the hash and compare. */
1296 | try {
1297 | byte[] computedHash = digest.digest(dgBytes);
1298 | VerificationStatus.HashMatchResult hashResult = new HashMatchResult(storedHash, computedHash);
1299 | hashResults.put(dgNumber, hashResult);
1300 |
1301 | if (!Arrays.equals(storedHash, computedHash)) {
1302 | verificationStatus.setHT(VerificationStatus.Verdict.FAILED, ReasonCode.HASH_MISMATCH_FAILURE, hashResults);
1303 | }
1304 |
1305 | return hashResult;
1306 | } catch (Exception ioe) {
1307 | VerificationStatus.HashMatchResult hashResult = new HashMatchResult(storedHash, null);
1308 | hashResults.put(dgNumber, hashResult);
1309 | verificationStatus.setHT(VerificationStatus.Verdict.FAILED, ReasonCode.UNEXPECTED_EXCEPTION_FAILURE, hashResults);
1310 | return hashResult;
1311 | }
1312 | }
1313 |
1314 | private MessageDigest getDigest(String digestAlgorithm) throws NoSuchAlgorithmException {
1315 | if (digest != null) {
1316 | digest.reset();
1317 | return digest;
1318 | }
1319 | LOGGER.info("Using hash algorithm " + digestAlgorithm);
1320 | if (Security.getAlgorithms("MessageDigest").contains(digestAlgorithm)) {
1321 | digest = MessageDigest.getInstance(digestAlgorithm);
1322 | } else {
1323 | digest = MessageDigest.getInstance(digestAlgorithm, BC_PROVIDER);
1324 | }
1325 | return digest;
1326 | }
1327 |
1328 | private List toDataGroupList(int[] tagList) {
1329 | if (tagList == null) { return null; }
1330 | List dgNumberList = new ArrayList(tagList.length);
1331 | for (int tag: tagList) {
1332 | try {
1333 | int dgNumber = LDSFileUtil.lookupDataGroupNumberByTag(tag);
1334 | dgNumberList.add(dgNumber);
1335 | } catch (NumberFormatException nfe) {
1336 | LOGGER.warning("Could not find DG number for tag: " + Integer.toHexString(tag));
1337 | nfe.printStackTrace();
1338 | }
1339 | }
1340 | return dgNumberList;
1341 | }
1342 | }
1343 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------