├── .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 | [![Get it on Google Play](http://www.tananaev.com/badges/google-play.svg)](https://play.google.com/store/apps/details?id=com.tananaev.passportreader) [![Get it on F-Droid](http://www.tananaev.com/badges/f-droid.svg)](https://f-droid.org/packages/com.tananaev.passportreader) 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 | 22 | 24 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 63 | 68 | 74 | 75 | 76 | 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 | --------------------------------------------------------------------------------