mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2024-12-22 20:40:16 +02:00
Merge remote-tracking branch 'origin/Tests' into Tests
# Conflicts: # MobileAuthApp/app/src/androidTest/java/com/tarkvaraprojekt/mobileauthapp/UC4Test.kt # MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt # MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java
This commit is contained in:
commit
67ba0ed764
@ -1,13 +1,10 @@
|
|||||||
package com.tarkvaraprojekt.mobileauthapp
|
package com.tarkvaraprojekt.mobileauthapp
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.nfc.NfcAdapter
|
import android.nfc.NfcAdapter
|
||||||
import android.nfc.tech.IsoDep
|
import android.nfc.tech.IsoDep
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -17,11 +14,14 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||||
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
|
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException
|
||||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
|
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||||
|
import java.io.IOException
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +69,6 @@ class AuthFragment : Fragment() {
|
|||||||
goToTheStart()
|
goToTheStart()
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
//binding!!.nextButton.visibility = View.INVISIBLE
|
|
||||||
binding!!.nextButton.setOnClickListener { goToNextFragment() }
|
binding!!.nextButton.setOnClickListener { goToNextFragment() }
|
||||||
binding!!.cancelButton.setOnClickListener { goToTheStart() }
|
binding!!.cancelButton.setOnClickListener { goToTheStart() }
|
||||||
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||||
@ -78,24 +77,19 @@ class AuthFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
||||||
adapter.enableReaderMode(activity, { tag ->
|
if (args.reading) {
|
||||||
timer.cancel()
|
adapter.enableReaderMode(activity, { tag ->
|
||||||
requireActivity().runOnUiThread {
|
timer.cancel()
|
||||||
binding!!.timeCounter.text = getString(R.string.card_detected)
|
requireActivity().runOnUiThread {
|
||||||
}
|
binding!!.timeCounter.text = getString(R.string.card_detected)
|
||||||
val card = IsoDep.get(tag)
|
}
|
||||||
card.timeout = 32768
|
var msgCode = 0
|
||||||
card.use {
|
|
||||||
try {
|
val card = IsoDep.get(tag)
|
||||||
val comms = Comms(it, viewModel.userCan)
|
card.timeout = 32768
|
||||||
if (args.auth) {
|
card.use {
|
||||||
val jws = Authenticator(comms).authenticate(
|
try {
|
||||||
intentParameters.challenge,
|
val comms = Comms(it, viewModel.userCan)
|
||||||
intentParameters.origin,
|
|
||||||
viewModel.userPin
|
|
||||||
)
|
|
||||||
intentParameters.setToken(jws)
|
|
||||||
} else {
|
|
||||||
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
|
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
|
||||||
viewModel.setUserFirstName(response[1])
|
viewModel.setUserFirstName(response[1])
|
||||||
viewModel.setUserLastName(response[0])
|
viewModel.setUserLastName(response[0])
|
||||||
@ -103,24 +97,45 @@ class AuthFragment : Fragment() {
|
|||||||
viewModel.setGender(response[3])
|
viewModel.setGender(response[3])
|
||||||
viewModel.setCitizenship(response[4])
|
viewModel.setCitizenship(response[4])
|
||||||
viewModel.setExpiration(response[5])
|
viewModel.setExpiration(response[5])
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
binding!!.timeCounter.text = getString(R.string.data_read)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: android.nfc.TagLostException) {
|
||||||
|
msgCode = R.string.tag_lost
|
||||||
|
} catch (e: InvalidCANException) {
|
||||||
|
msgCode = R.string.invalid_can
|
||||||
|
// If the CAN is wrong we will also delete the saved CAN so that the user won't use it again.
|
||||||
|
viewModel.deleteCan(requireContext())
|
||||||
|
} catch (e: AuthAppException) {
|
||||||
|
msgCode = when (e.code) {
|
||||||
|
448 -> R.string.err_bad_data
|
||||||
|
500 -> R.string.err_internal
|
||||||
|
else -> R.string.err_unknown
|
||||||
|
}
|
||||||
|
} catch (e: GeneralSecurityException) {
|
||||||
|
msgCode = R.string.err_internal
|
||||||
|
} catch (e: IOException) {
|
||||||
|
msgCode = R.string.err_reading_card
|
||||||
|
} catch (e: Exception) {
|
||||||
|
msgCode = R.string.err_unknown
|
||||||
|
} finally {
|
||||||
|
adapter.disableReaderMode(activity)
|
||||||
}
|
}
|
||||||
requireActivity().runOnUiThread {
|
|
||||||
binding!!.timeCounter.text = getString(R.string.data_read)
|
if (msgCode != 0) {
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
binding!!.timeCounter.text = getString(msgCode)
|
||||||
|
}
|
||||||
|
// Gives user some time to read the error message
|
||||||
|
Thread.sleep(1000)
|
||||||
|
goToTheStart()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
requireActivity().runOnUiThread {
|
|
||||||
binding!!.timeCounter.text = getString(R.string.no_success)
|
|
||||||
}
|
|
||||||
// If the CAN is wrong we will also delete the saved CAN so that the user won't use it again.
|
|
||||||
viewModel.deleteCan(requireContext())
|
|
||||||
// Gives user some time to read the error message
|
|
||||||
Thread.sleep(1000)
|
|
||||||
goToTheStart()
|
|
||||||
} finally {
|
|
||||||
adapter.disableReaderMode(activity)
|
|
||||||
}
|
}
|
||||||
}
|
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
||||||
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
} else { //We want to create a JWT instead of reading the info from the card.
|
||||||
|
goToNextFragment()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goToNextFragment() {
|
private fun goToNextFragment() {
|
||||||
@ -141,7 +156,8 @@ class AuthFragment : Fragment() {
|
|||||||
} else {
|
} else {
|
||||||
if (!args.mobile) {
|
if (!args.mobile) {
|
||||||
//Currently for some reason the activity is not killed entirely. Must be looked into further.
|
//Currently for some reason the activity is not killed entirely. Must be looked into further.
|
||||||
requireActivity().finishAndRemoveTask()
|
requireActivity().finish()
|
||||||
|
exitProcess(0)
|
||||||
} else {
|
} else {
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||||
|
@ -3,6 +3,9 @@ package com.tarkvaraprojekt.mobileauthapp.NFC;
|
|||||||
import android.nfc.tech.IsoDep;
|
import android.nfc.tech.IsoDep;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException;
|
||||||
|
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.BlockCipher;
|
import org.bouncycastle.crypto.BlockCipher;
|
||||||
import org.bouncycastle.crypto.engines.AESEngine;
|
import org.bouncycastle.crypto.engines.AESEngine;
|
||||||
import org.bouncycastle.crypto.macs.CMac;
|
import org.bouncycastle.crypto.macs.CMac;
|
||||||
@ -21,7 +24,6 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
@ -31,43 +33,47 @@ import javax.crypto.spec.IvParameterSpec;
|
|||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
public class Comms {
|
public class Comms {
|
||||||
|
private static final byte[] master = { // select Main AID
|
||||||
|
0, -92, 4, 12, 16, -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] selectMaster = Hex.decode("00a4040c10a000000077010800070000fe00000100");
|
private static final byte[] MSESetAT = { // manage security environment: set authentication template
|
||||||
|
0, 34, -63, -92, 15, -128, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -125, 1, 2, 0
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] MSESetAT = Hex.decode("0022c1a40f800a04007f0007020204020483010200");
|
private static final byte[] GAGetNonce = { // general authenticate: get nonce
|
||||||
|
16, -122, 0, 0, 2, 124, 0, 0
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAGetNonce = Hex.decode("10860000027c0000");
|
private static final byte[] GAMapNonceIncomplete = {
|
||||||
|
16, -122, 0, 0, 69, 124, 67, -127, 65
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAMapNonceIncomplete = Hex.decode("10860000457c438141");
|
private static final byte[] GAKeyAgreementIncomplete = {
|
||||||
|
16, -122, 0, 0, 69, 124, 67, -125, 65
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAKeyAgreementIncomplete = Hex.decode("10860000457c438341");
|
private static final byte[] GAMutualAuthenticationIncomplete = {
|
||||||
|
0, -122, 0, 0, 12, 124, 10, -123, 8
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] GAMutualAuthenticationIncomplete = Hex.decode("008600000c7c0a8508");
|
private static final byte[] dataForMACIncomplete = {
|
||||||
|
127, 73, 79, 6, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -122, 65
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641");
|
private static final byte[] masterSec = {
|
||||||
|
12, -92, 4, 12, 45, -121, 33, 1
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] selectFile = Hex.decode("0ca4010c1d871101");
|
private static final byte[] personal = { // select personal data DF
|
||||||
|
12, -92, 1, 12, 29, -121, 17, 1
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] readFile = Hex.decode("0cb000000d970100");
|
private static final byte[] read = { // read binary
|
||||||
|
12, -80, 0, 0, 13, -105, 1, 0
|
||||||
|
};
|
||||||
|
|
||||||
private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101");
|
private IsoDep idCard;
|
||||||
|
|
||||||
private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101");
|
|
||||||
|
|
||||||
private static final byte[] MSESetEnv = Hex.decode("0c2241A41d871101");
|
|
||||||
|
|
||||||
private static final byte[] Env = Hex.decode("8004FF200800840181");
|
|
||||||
|
|
||||||
private static final byte[] InternalAuthenticate = Hex.decode("0c8800001d871101");
|
|
||||||
|
|
||||||
private static final byte[] IASECCFID = {0x3f, 0x00};
|
|
||||||
private static final byte[] personalDF = {0x50, 0x00};
|
|
||||||
private static final byte[] AWP = {(byte) 0xad, (byte) 0xf1};
|
|
||||||
private static final byte[] QSCD = {(byte) 0xad, (byte) 0xf2};
|
|
||||||
private static final byte[] authCert = {0x34, 0x01};
|
|
||||||
private static final byte[] signCert = {0x34, 0x1f};
|
|
||||||
|
|
||||||
private final IsoDep idCard;
|
|
||||||
private final byte[] keyEnc;
|
private final byte[] keyEnc;
|
||||||
private final byte[] keyMAC;
|
private final byte[] keyMAC;
|
||||||
private byte ssc; // Send sequence counter.
|
private byte ssc; // Send sequence counter.
|
||||||
@ -81,12 +87,21 @@ public class Comms {
|
|||||||
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||||
|
|
||||||
idCard.connect();
|
idCard.connect();
|
||||||
|
|
||||||
this.idCard = idCard;
|
this.idCard = idCard;
|
||||||
byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
byte[][] keys = PACE(CAN);
|
||||||
|
Log.i("Pace duration", String.valueOf(System.currentTimeMillis() - start));
|
||||||
|
|
||||||
keyEnc = keys[0];
|
keyEnc = keys[0];
|
||||||
keyMAC = keys[1];
|
keyMAC = keys[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getAuthenticationCertificate() {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the message authentication code
|
* Calculates the message authentication code
|
||||||
*
|
*
|
||||||
@ -139,51 +154,40 @@ public class Comms {
|
|||||||
* @param CAN the card access number provided by the user
|
* @param CAN the card access number provided by the user
|
||||||
* @return the decrypted nonce
|
* @return the decrypted nonce
|
||||||
*/
|
*/
|
||||||
private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||||
byte[] decryptionKey = createKey(CAN, (byte) 3);
|
byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3);
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||||
return cipher.doFinal(encryptedNonce);
|
return cipher.doFinal(encryptedNonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Communicates with the card and logs the response
|
|
||||||
*
|
|
||||||
* @param APDU The command
|
|
||||||
* @param log Information for logging
|
|
||||||
* @return The response
|
|
||||||
*/
|
|
||||||
private byte[] getResponse(byte[] APDU, String log) throws IOException {
|
|
||||||
byte[] response = idCard.transceive(APDU);
|
|
||||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
|
||||||
throw new RuntimeException(String.format("%s failed.", log));
|
|
||||||
}
|
|
||||||
Log.i(log, Hex.toHexString(response));
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
|
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
|
||||||
*
|
*
|
||||||
* @param CAN the card access number
|
* @param CAN the card access number
|
||||||
*/
|
*/
|
||||||
private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
||||||
|
|
||||||
// select the IAS-ECC application on the chip
|
// select the ECC applet on the chip
|
||||||
getResponse(selectMaster, "Select the master application");
|
byte[] response = idCard.transceive(master);
|
||||||
|
Log.i("Select applet", Hex.toHexString(response));
|
||||||
|
|
||||||
// initiate PACE
|
// initiate PACE
|
||||||
getResponse(MSESetAT, "Set authentication template");
|
response = idCard.transceive(MSESetAT);
|
||||||
|
Log.i("Authentication template", Hex.toHexString(response));
|
||||||
|
|
||||||
// get nonce
|
// get nonce
|
||||||
byte[] response = getResponse(GAGetNonce, "Get nonce");
|
response = idCard.transceive(GAGetNonce);
|
||||||
|
Log.i("Get nonce", Hex.toHexString(response));
|
||||||
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
|
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
|
||||||
|
|
||||||
// generate an EC keypair and exchange public keys with the chip
|
// generate an EC keypair and exchange public keys with the chip
|
||||||
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
|
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
|
||||||
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
|
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
|
||||||
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
|
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
|
||||||
response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce");
|
byte[] APDU = createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i("Map nonce", Hex.toHexString(response));
|
||||||
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||||
|
|
||||||
// calculate the new base point, use it to generate a new keypair, and exchange public keys
|
// calculate the new base point, use it to generate a new keypair, and exchange public keys
|
||||||
@ -191,41 +195,35 @@ public class Comms {
|
|||||||
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
|
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
|
||||||
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
|
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
|
||||||
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
|
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
|
||||||
response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement");
|
APDU = createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i("Key agreement", Hex.toHexString(response));
|
||||||
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||||
|
|
||||||
// generate the session keys and exchange MACs to verify them
|
// generate the session keys and exchange MACs to verify them
|
||||||
byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
|
sharedSecret = cardPublicKey.multiply(privateKey).normalize();
|
||||||
byte[] keyEnc = createKey(secret, (byte) 1);
|
byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded();
|
||||||
byte[] keyMAC = createKey(secret, (byte) 2);
|
byte[] keyEnc = createKey(encodedSecret, (byte) 1);
|
||||||
byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
|
byte[] keyMAC = createKey(encodedSecret, (byte) 2);
|
||||||
response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
|
APDU = createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65);
|
||||||
|
byte[] MAC = getMAC(APDU, keyMAC);
|
||||||
|
APDU = createAPDU(GAMutualAuthenticationIncomplete, MAC, 9);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i("Mutual authentication", Hex.toHexString(response));
|
||||||
|
|
||||||
// verify chip's MAC and return session keys
|
// if the chip-side verification fails, crash and burn
|
||||||
MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
|
if (response.length == 2) throw new InvalidCANException();
|
||||||
|
|
||||||
|
// otherwise verify chip's MAC and return session keys
|
||||||
|
APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65);
|
||||||
|
MAC = getMAC(APDU, keyMAC);
|
||||||
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
|
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
|
||||||
throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen.
|
throw new AuthAppException("Could not verify chip's MAC.", 448); // Should never happen.
|
||||||
}
|
}
|
||||||
return new byte[][]{keyEnc, keyMAC};
|
return new byte[][]{keyEnc, keyMAC};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects a file and reads its contents
|
|
||||||
*
|
|
||||||
* @param FID file identifier of the required file
|
|
||||||
* @param info string for logging
|
|
||||||
* @return decrypted file contents
|
|
||||||
*/
|
|
||||||
private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
|
||||||
selectFile(FID, info);
|
|
||||||
byte[] response = getResponse(new byte[0], readFile, "Read binary");
|
|
||||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
|
||||||
throw new RuntimeException(String.format("Could not read %s", info));
|
|
||||||
}
|
|
||||||
return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts or decrypts the APDU data
|
* Encrypts or decrypts the APDU data
|
||||||
*
|
*
|
||||||
@ -260,182 +258,74 @@ public class Comms {
|
|||||||
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
|
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
|
||||||
macData[15] = ssc; // first block contains the ssc
|
macData[15] = ssc; // first block contains the ssc
|
||||||
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
|
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
|
||||||
macData[20] = (byte) 0x80; // elements are terminated by 0x80 and zero-padded to the next block
|
macData[20] = -128; // elements are terminated by 0x80 and zero-padded to the next block
|
||||||
System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le
|
System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le
|
||||||
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
|
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
|
||||||
byte[] paddedData = Arrays.copyOf(data, length);
|
byte[] paddedData = Arrays.copyOf(data, length);
|
||||||
paddedData[data.length] = (byte) 0x80;
|
paddedData[data.length] = -128;
|
||||||
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
|
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
|
||||||
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
|
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
|
||||||
}
|
}
|
||||||
macData[35 + encryptedData.length] = (byte) 0x80;
|
macData[35 + encryptedData.length] = -128;
|
||||||
byte[] MAC = getMAC(macData, keyMAC);
|
byte[] MAC = getMAC(macData, keyMAC);
|
||||||
|
|
||||||
// construct the APDU using the encrypted data and the MAC
|
// construct the APDU using the encrypted data and the MAC
|
||||||
byte[] APDU = Arrays.copyOf(incomplete, incomplete.length + encryptedData.length + MAC.length + 3);
|
byte[] APDU = new byte[incomplete.length + encryptedData.length + MAC.length + 3];
|
||||||
|
System.arraycopy(incomplete, 0, APDU, 0, incomplete.length);
|
||||||
if (encryptedData.length > 0) {
|
if (encryptedData.length > 0) {
|
||||||
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
|
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
|
||||||
}
|
}
|
||||||
System.arraycopy(new byte[]{(byte) 0x8E, 0x08}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
|
System.arraycopy(new byte[]{-114, 8}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
|
||||||
System.arraycopy(MAC, 0, APDU, incomplete.length + encryptedData.length + 2, MAC.length);
|
System.arraycopy(MAC, 0, APDU, incomplete.length + encryptedData.length + 2, MAC.length);
|
||||||
|
|
||||||
ssc++;
|
ssc++;
|
||||||
return APDU;
|
return APDU;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects a FILE by its identifier
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
|
||||||
byte[] response = getResponse(FID, selectFile, String.format("Select %s", info));
|
|
||||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
|
||||||
throw new RuntimeException(String.format("Could not select %s", info));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the contents of the personal data dedicated file
|
* Gets the contents of the personal data dedicated file
|
||||||
*
|
*
|
||||||
* @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16)
|
* @param FID the last bytes of file identifiers being requested
|
||||||
* @return array containing the corresponding data strings
|
* @return array containing the data strings
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public String[] readPersonalData(byte[] lastBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
public String[] readPersonalData(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||||
|
|
||||||
String[] personalData = new String[lastBytes.length];
|
String[] personalData = new String[FID.length];
|
||||||
int stringIndex = 0;
|
byte[] data;
|
||||||
|
byte[] APDU;
|
||||||
// select the master application
|
byte[] response;
|
||||||
selectFile(IASECCFID, "the master application");
|
|
||||||
|
|
||||||
// select the personal data dedicated file
|
// select the personal data dedicated file
|
||||||
selectFile(personalDF, "the personal data DF");
|
data = new byte[]{80, 0}; // personal data DF FID
|
||||||
|
APDU = createSecureAPDU(data, personal);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i("Select personal data DF", Hex.toHexString(response));
|
||||||
|
|
||||||
byte[] FID = Arrays.copyOf(personalDF, personalDF.length);
|
// select and read the first 8 elementary files in the DF
|
||||||
// select and read the personal data elementary files
|
for (int i = 0; i < FID.length; i++) {
|
||||||
for (byte index : lastBytes) {
|
|
||||||
|
|
||||||
if (index > 15 || index < 1) throw new RuntimeException("Invalid personal data FID.");
|
byte index = FID[i];
|
||||||
FID[1] = index;
|
if (index > 15 || index < 1) throw new AuthAppException("Invalid personal data FID.", 500);
|
||||||
|
|
||||||
|
data[1] = index;
|
||||||
|
APDU = createSecureAPDU(data, personal);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i(String.format("Select EF 500%d", index), Hex.toHexString(response));
|
||||||
|
|
||||||
|
APDU = createSecureAPDU(new byte[0], read);
|
||||||
|
response = idCard.transceive(APDU);
|
||||||
|
Log.i(String.format("Read binary EF 500%d", index), Hex.toHexString(response));
|
||||||
|
|
||||||
// store the decrypted datum
|
// store the decrypted datum
|
||||||
byte[] response = readFile(FID, "a personal data EF");
|
byte[] raw = encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
|
||||||
int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2;
|
int indexOfTerminator = Hex.toHexString(raw).lastIndexOf("80") / 2;
|
||||||
personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator));
|
personalData[i] = new String(Arrays.copyOfRange(raw, 0, indexOfTerminator));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return personalData;
|
return personalData;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to verify the selected PIN
|
|
||||||
*
|
|
||||||
* @param PIN user-provided PIN
|
|
||||||
* @param oneOrTwo true for PIN1, false for PIN2
|
|
||||||
*/
|
|
||||||
private void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
|
|
||||||
|
|
||||||
selectFile(IASECCFID, "the master application");
|
|
||||||
if (!oneOrTwo) {
|
|
||||||
selectFile(QSCD, "the application");
|
|
||||||
}
|
|
||||||
|
|
||||||
// pad the PIN and use the chip for verification
|
|
||||||
byte[] paddedPIN = Hex.decode("ffffffffffffffffffffffff");
|
|
||||||
System.arraycopy(PIN, 0, paddedPIN, 0, PIN.length);
|
|
||||||
byte[] response = getResponse(paddedPIN, oneOrTwo ? verifyPIN1 : verifyPIN2, "PIN verification");
|
|
||||||
|
|
||||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
|
||||||
if (response[response.length - 2] == 0x69 && response[response.length - 1] == (byte) 0x83) {
|
|
||||||
throw new RuntimeException("Invalid PIN. Authentication method blocked.");
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException(String.format("Invalid PIN. Attempts left: %d.", response[response.length - 1] + 64));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the authentication or signature certificate from the chip
|
|
||||||
*
|
|
||||||
* @param authOrSign true for auth, false for sign cert
|
|
||||||
* @return the requested certificate
|
|
||||||
*/
|
|
||||||
public byte[] getCertificate(boolean authOrSign) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
|
|
||||||
|
|
||||||
selectFile(IASECCFID, "the master application");
|
|
||||||
|
|
||||||
selectFile(authOrSign ? AWP : QSCD, "the application");
|
|
||||||
|
|
||||||
selectFile(authOrSign ? authCert : signCert, "the certificate");
|
|
||||||
|
|
||||||
byte[] certificate = new byte[0];
|
|
||||||
byte[] readCert = Arrays.copyOf(readFile, readFile.length);
|
|
||||||
// Construct the certificate byte array n=indexOfTerminator bytes at a time
|
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
|
|
||||||
// Set the P1/P2 values to incrementally read the certificate
|
|
||||||
readCert[2] = (byte) (certificate.length / 256);
|
|
||||||
readCert[3] = (byte) (certificate.length % 256);
|
|
||||||
byte[] response = getResponse(new byte[0], readCert, "Read the certificate");
|
|
||||||
if (response[response.length - 2] == 0x6b && response[response.length - 1] == 0x00) {
|
|
||||||
throw new RuntimeException("Wrong read parameters.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the range containing a portion of the certificate and decrypt it
|
|
||||||
int start = response[2] == 1 ? 3 : 4;
|
|
||||||
int end = start + (response[start - 2] + 256) % 256 - 1;
|
|
||||||
byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, start, end), Cipher.DECRYPT_MODE);
|
|
||||||
int indexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2;
|
|
||||||
certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator);
|
|
||||||
System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator);
|
|
||||||
|
|
||||||
if (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == 0x00) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificate;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs the authentication token hash
|
|
||||||
*
|
|
||||||
* @param PIN1 PIN1
|
|
||||||
* @param token the token hash to be signed
|
|
||||||
* @return authentication token hash signature
|
|
||||||
*/
|
|
||||||
public byte[] authenticate(String PIN1, byte[] token) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
|
||||||
|
|
||||||
verifyPIN(PIN1.getBytes(StandardCharsets.UTF_8), true);
|
|
||||||
|
|
||||||
selectFile(AWP, "the AWP application");
|
|
||||||
|
|
||||||
byte[] response = getResponse(Env, MSESetEnv, "Set environment");
|
|
||||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
|
||||||
throw new RuntimeException("Setting the environment failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
InternalAuthenticate[4] = (byte) (0x1d + 16 * (token.length / 16));
|
|
||||||
InternalAuthenticate[6] = (byte) (0x11 + 16 * (token.length / 16));
|
|
||||||
response = getResponse(token, InternalAuthenticate, "Internal Authenticate");
|
|
||||||
|
|
||||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
|
||||||
throw new RuntimeException("Signing the token failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] signature = encryptDecryptData(Arrays.copyOfRange(response, 3, 115), Cipher.DECRYPT_MODE);
|
|
||||||
int indexOfTerminator = Hex.toHexString(signature).lastIndexOf("80") / 2;
|
|
||||||
|
|
||||||
return Arrays.copyOf(signature, indexOfTerminator);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private byte[] getResponse(byte[] data, byte[] command, String log) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
|
||||||
byte[] response = idCard.transceive(createSecureAPDU(data, command));
|
|
||||||
Log.i(log, Hex.toHexString(response));
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user