diff --git a/MobileAuthApp/app/build.gradle b/MobileAuthApp/app/build.gradle index a65bc6e..bc4a034 100644 --- a/MobileAuthApp/app/build.gradle +++ b/MobileAuthApp/app/build.gradle @@ -62,6 +62,13 @@ dependencies { //SecureDataStoring implementation("androidx.security:security-crypto:1.0.0") + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', + 'org.bouncycastle:bcprov-jdk15on:1.60', + 'io.jsonwebtoken:jjwt-gson:0.11.2' + + implementation 'com.koushikdutta.ion:ion:3.1.0' + // Retrofit + Moshi Converter implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' implementation 'com.squareup.moshi:moshi-kotlin:1.9.3' diff --git a/MobileAuthApp/app/src/main/AndroidManifest.xml b/MobileAuthApp/app/src/main/AndroidManifest.xml index d13134c..e9b29ca 100644 --- a/MobileAuthApp/app/src/main/AndroidManifest.xml +++ b/MobileAuthApp/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.tarkvaraprojekt.mobileauthapp"> + - timer.cancel() - requireActivity().runOnUiThread { - binding!!.timeCounter.text = getString(R.string.card_detected) - } - val card = IsoDep.get(tag) - card.timeout = 32768 - card.use { - try { - val comms = Comms(it, viewModel.userCan) + adapter.enableReaderMode(activity, { tag -> + timer.cancel() + requireActivity().runOnUiThread { + binding!!.timeCounter.text = getString(R.string.card_detected) + } + val card = IsoDep.get(tag) + card.timeout = 32768 + card.use { + try { + val comms = Comms(it, viewModel.userCan) + if (args.auth) { + val jws = Authenticator(comms).authenticate( + intentParameters.challenge, + intentParameters.origin, + viewModel.userPin + ) + intentParameters.setToken(jws) + } else { val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8)) viewModel.setUserFirstName(response[1]) viewModel.setUserLastName(response[0]) @@ -92,26 +103,24 @@ class AuthFragment : Fragment() { viewModel.setGender(response[3]) viewModel.setCitizenship(response[4]) viewModel.setExpiration(response[5]) - requireActivity().runOnUiThread { - binding!!.timeCounter.text = getString(R.string.data_read) - } - } 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) } + requireActivity().runOnUiThread { + binding!!.timeCounter.text = getString(R.string.data_read) + } + } 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) - } else { //We want to create a JWT instead of reading the info from the card. - goToNextFragment() - } + } + }, NfcAdapter.FLAG_READER_NFC_A, null) } private fun goToNextFragment() { @@ -132,8 +141,7 @@ class AuthFragment : Fragment() { } else { if (!args.mobile) { //Currently for some reason the activity is not killed entirely. Must be looked into further. - requireActivity().finish() - exitProcess(0) + requireActivity().finishAndRemoveTask() } else { val resultIntent = Intent() requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt index 7ad1207..1b281aa 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/CanFragment.kt @@ -127,10 +127,14 @@ class CanFragment : Fragment() { // TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment. if (args.saving) { findNavController().navigate(R.id.action_canFragment_to_settingsFragment) - } else if (args.auth) { - val resultIntent = Intent() - requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) - requireActivity().finish() + } else if (args.auth || args.mobile) { + if (args.mobile) { + val resultIntent = Intent() + requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) + requireActivity().finish() + } else { + requireActivity().finishAndRemoveTask() + } } else { findNavController().navigate(R.id.action_canFragment_to_homeFragment) } diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt index e79c402..e1888f4 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt @@ -14,6 +14,7 @@ import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import java.lang.Exception +import java.net.URLDecoder /** * HomeFragment is only shown to the user when then the user launches the application. When the application @@ -30,6 +31,7 @@ class HomeFragment : Fragment() { private var binding: FragmentHomeBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -56,11 +58,14 @@ class HomeFragment : Fragment() { // We use !! because we want an exception when something is not right. intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!) intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!) + intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!) } else { //Website - // Currently the test website won't send the authUrl parameter - //Log.i("intentDebugging", requireActivity().intent.data.toString()) - intentParams.setChallenge(requireActivity().intent.data!!.getQueryParameter("challenge")!!) - //intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!) + var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!! + // TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back. + challenge = challenge.replace(" ", "+") + intentParams.setChallenge(challenge) + intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!) + intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!) } } catch (e: Exception) { // There was a problem with parameters, which means that authentication is not possible. diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java index a4d184b..bccb6ee 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java @@ -21,6 +21,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; +import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -30,47 +31,43 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; 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[] 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[] selectMaster = Hex.decode("00a4040c10a000000077010800070000fe00000100"); - private static final byte[] GAGetNonce = { // general authenticate: get nonce - 16, -122, 0, 0, 2, 124, 0, 0 - }; + private static final byte[] MSESetAT = Hex.decode("0022c1a40f800a04007f0007020204020483010200"); - private static final byte[] GAMapNonceIncomplete = { - 16, -122, 0, 0, 69, 124, 67, -127, 65 - }; + private static final byte[] GAGetNonce = Hex.decode("10860000027c0000"); - private static final byte[] GAKeyAgreementIncomplete = { - 16, -122, 0, 0, 69, 124, 67, -125, 65 - }; + private static final byte[] GAMapNonceIncomplete = Hex.decode("10860000457c438141"); - private static final byte[] GAMutualAuthenticationIncomplete = { - 0, -122, 0, 0, 12, 124, 10, -123, 8 - }; + private static final byte[] GAKeyAgreementIncomplete = Hex.decode("10860000457c438341"); - 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[] GAMutualAuthenticationIncomplete = Hex.decode("008600000c7c0a8508"); - private static final byte[] masterSec = { - 12, -92, 4, 12, 45, -121, 33, 1 - }; + private static final byte[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641"); - private static final byte[] personal = { // select personal data DF - 12, -92, 1, 12, 29, -121, 17, 1 - }; + private static final byte[] selectFile = Hex.decode("0ca4010c1d871101"); - private static final byte[] read = { // read binary - 12, -80, 0, 0, 13, -105, 1, 0 - }; + private static final byte[] readFile = Hex.decode("0cb000000d970100"); - private IsoDep idCard; + private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101"); + + 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[] keyMAC; private byte ssc; // Send sequence counter. @@ -84,21 +81,12 @@ public class Comms { public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { idCard.connect(); - this.idCard = idCard; - - long start = System.currentTimeMillis(); - byte[][] keys = PACE(CAN); - Log.i("Pace duration", String.valueOf(System.currentTimeMillis() - start)); - + byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8)); keyEnc = keys[0]; keyMAC = keys[1]; } - public byte[] getAuthenticationCertificate() { - return new byte[0]; - } - /** * Calculates the message authentication code * @@ -151,40 +139,51 @@ public class Comms { * @param CAN the card access number provided by the user * @return the decrypted nonce */ - private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3); + private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { + byte[] decryptionKey = createKey(CAN, (byte) 3); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16])); 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 * * @param CAN the card access number */ - private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { + private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { - // select the ECC applet on the chip - byte[] response = idCard.transceive(master); - Log.i("Select applet", Hex.toHexString(response)); + // select the IAS-ECC application on the chip + getResponse(selectMaster, "Select the master application"); // initiate PACE - response = idCard.transceive(MSESetAT); - Log.i("Authentication template", Hex.toHexString(response)); + getResponse(MSESetAT, "Set authentication template"); // get nonce - response = idCard.transceive(GAGetNonce); - Log.i("Get nonce", Hex.toHexString(response)); + byte[] response = getResponse(GAGetNonce, "Get nonce"); byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN); // generate an EC keypair and exchange public keys with the chip 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 ECPoint publicKey = spec.getG().multiply(privateKey).normalize(); - byte[] APDU = createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66); - response = idCard.transceive(APDU); - Log.i("Map nonce", Hex.toHexString(response)); + response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce"); 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 @@ -192,35 +191,41 @@ public class Comms { ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize(); privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); publicKey = mappedECBasePoint.multiply(privateKey).normalize(); - APDU = createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66); - response = idCard.transceive(APDU); - Log.i("Key agreement", Hex.toHexString(response)); + response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement"); cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69)); // generate the session keys and exchange MACs to verify them - sharedSecret = cardPublicKey.multiply(privateKey).normalize(); - byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded(); - byte[] keyEnc = createKey(encodedSecret, (byte) 1); - byte[] keyMAC = createKey(encodedSecret, (byte) 2); - 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)); + byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded(); + byte[] keyEnc = createKey(secret, (byte) 1); + byte[] keyMAC = createKey(secret, (byte) 2); + byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC); + response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication"); - // if the chip-side verification fails, crash and burn - if (response.length == 2) throw new RuntimeException("Invalid CAN."); - - // otherwise verify chip's MAC and return session keys - APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65); - MAC = getMAC(APDU, keyMAC); + // verify chip's MAC and return session keys + MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC); if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) { - throw new RuntimeException("Could not verify chip's MAC."); // Should never happen. + throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen. } 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 * @@ -255,74 +260,182 @@ public class Comms { byte[] macData = new byte[data.length > 0 ? 48 + length : 48]; macData[15] = ssc; // first block contains the ssc System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command - macData[20] = -128; // elements are terminated by 0x80 and zero-padded to the next block + macData[20] = (byte) 0x80; // 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 if (data.length > 0) { // if the APDU has data, add padding and encrypt it byte[] paddedData = Arrays.copyOf(data, length); - paddedData[data.length] = -128; + paddedData[data.length] = (byte) 0x80; encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE); System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length); } - macData[35 + encryptedData.length] = -128; + macData[35 + encryptedData.length] = (byte) 0x80; byte[] MAC = getMAC(macData, keyMAC); // construct the APDU using the encrypted data and the MAC - byte[] APDU = new byte[incomplete.length + encryptedData.length + MAC.length + 3]; - System.arraycopy(incomplete, 0, APDU, 0, incomplete.length); + byte[] APDU = Arrays.copyOf(incomplete, incomplete.length + encryptedData.length + MAC.length + 3); if (encryptedData.length > 0) { System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length); } - System.arraycopy(new byte[]{-114, 8}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E + System.arraycopy(new byte[]{(byte) 0x8E, 0x08}, 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); - ssc++; return APDU; } /** - * Gets the contents of the personal data dedicated file - * - * @param FID the last bytes of file identifiers being requested - * @return array containing the data strings + * Selects a FILE by its identifier * */ - public String[] readPersonalData(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + 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)); + } + } - String[] personalData = new String[FID.length]; - byte[] data; - byte[] APDU; - byte[] response; + /** + * Gets the contents of the personal data dedicated file + * + * @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16) + * @return array containing the corresponding data strings + */ + public String[] readPersonalData(byte[] lastBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + + String[] personalData = new String[lastBytes.length]; + int stringIndex = 0; + + // select the master application + selectFile(IASECCFID, "the master application"); // select the personal data dedicated file - 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)); + selectFile(personalDF, "the personal data DF"); - // select and read the first 8 elementary files in the DF - for (int i = 0; i < FID.length; i++) { + byte[] FID = Arrays.copyOf(personalDF, personalDF.length); + // select and read the personal data elementary files + for (byte index : lastBytes) { - byte index = FID[i]; if (index > 15 || index < 1) throw new RuntimeException("Invalid personal data FID."); - - 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)); + FID[1] = index; // store the decrypted datum - byte[] raw = encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE); - int indexOfTerminator = Hex.toHexString(raw).lastIndexOf("80") / 2; - personalData[i] = new String(Arrays.copyOfRange(raw, 0, indexOfTerminator)); + byte[] response = readFile(FID, "a personal data EF"); + int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2; + personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator)); } - 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; + } + } diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt index d6a78a4..86dbbc1 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/PinFragment.kt @@ -128,10 +128,14 @@ class PinFragment : Fragment() { private fun goToTheStart() { if (args.saving) { findNavController().navigate(R.id.action_canFragment_to_settingsFragment) - } else if (args.auth) { - val resultIntent = Intent() - requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) - requireActivity().finish() + } else if (args.auth || args.mobile) { + if (args.mobile) { + val resultIntent = Intent() + requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) + requireActivity().finish() + } else { + requireActivity().finishAndRemoveTask() + } } else { findNavController().navigate(R.id.action_canFragment_to_homeFragment) } diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt index 8c61a02..e658c72 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt @@ -10,15 +10,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import com.google.gson.JsonObject +import com.koushikdutta.ion.Ion import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel -import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel -import com.tarkvaraprojekt.mobileauthapp.network.TokenApi -import com.tarkvaraprojekt.mobileauthapp.network.TokenItem -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlin.system.exitProcess /** * ResultFragment is used to create a JWT and to send response to the website/application @@ -44,44 +39,52 @@ class ResultFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding!!.resultBackButton.setOnClickListener { - if (args.mobile) { - createResponse() - } - } + binding!!.resultBackButton.visibility = View.GONE + postToken() } /** * Makes a POST request to the backend server with a tokenItem */ fun postToken() { - val tokenData = TokenItem( - paramsModel.token, - paramsModel.challenge - ) - CoroutineScope(Dispatchers.Default).launch { - val response = TokenApi.retrofitService.postToken(tokenData) - if (response.isSuccessful) { - //Success scenario here - } else { - //Failure scenario here - if (args.mobile) { - createResponse(false) - } else { - //Currently for some reason the activity is not killed entirely. Must be looked into further. - requireActivity().finish() - exitProcess(0) + val json = JsonObject() + json.addProperty("token", paramsModel.token) + json.addProperty("challenge", paramsModel.challenge) + + Ion.getDefault(activity).conscryptMiddleware.enable(false) + Ion.with(activity) + .load(paramsModel.origin + paramsModel.authUrl) + .setJsonObjectBody(json) + .asJsonObject() + .setCallback { e, result -> + // do stuff with the result or error + if (result == null) { + // TODO: Set auth message failed and close the app + Log.i("Log thingy fail", "result was null") + if (args.mobile) { + createResponse(false) + } else { + requireActivity().finishAndRemoveTask() + } + } else { + Log.i("POST request response", result.toString()) + if (args.mobile) { + createResponse(true, result.toString(), paramsModel.token) + } else { + requireActivity().finishAndRemoveTask() + } + } } - } - } } /** * Only used when the MobileAuthApp was launched by an app. Not for website use. */ - private fun createResponse(success: Boolean = true) { + private fun createResponse(success: Boolean = true, result: String = "noResult", token: String = "noToken") { val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED val resultIntent = Intent() + resultIntent.putExtra("result", result) + resultIntent.putExtra("token", token) requireActivity().setResult(responseCode, resultIntent) requireActivity().finish() } diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt index a92d716..023602b 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt @@ -1,22 +1,59 @@ package com.tarkvaraprojekt.mobileauthapp.auth -import android.nfc.tech.IsoDep +import android.util.Log import com.tarkvaraprojekt.mobileauthapp.NFC.Comms -import java.math.BigInteger +import io.jsonwebtoken.SignatureAlgorithm +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.time.LocalDateTime +import java.time.ZoneOffset -class Authenticator(val comms : Comms) { +class Authenticator(val comms: Comms) { - public fun authenticate(nonce: BigInteger, challengeUrl: String, pin1: String) { + val type = "JWT" + val algorithm = "ES384" + var iss = "https://self-issued.me" // Will be specified at a later date. + val algorithmUsedForSigning = SignatureAlgorithm.ES384 + + fun authenticate(challenge: String, originUrl: String, pin1: String): String { // Ask PIN 1 from the user and get the authentication certificate from the ID card. - val authenticationCertificate : ByteArray = comms.getAuthenticationCertificate(); + val authenticationCertificate: ByteArray = comms.getCertificate(true); - // Create the authentication token (OpenID X509) + // Encode the certificate in base64. + val base64cert = java.util.Base64.getEncoder().encodeToString(authenticationCertificate) - // Hash the authentication token. + // Get current epoch time. + val epoch = LocalDateTime.now(ZoneOffset.UTC).atZone(ZoneOffset.UTC).toEpochSecond() + + // Get expiration time. + val exp = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(5 * 60L).atZone(ZoneOffset.UTC) + .toEpochSecond() + + // TODO: Get subject value. + val sub = "FAMILYNAME.NAME" + + // Get header and claims. + val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}""" + val claims = + """{"iat":"$epoch","exp":"$exp","aud":["$originUrl"],"iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}""" + + val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode( + claims.toByteArray(Charsets.UTF_8) + ) // Send the authentication token hash to the ID card for signing and get signed authentication token as response. + val encoded = + MessageDigest.getInstance("SHA-384").digest(jwt.toByteArray(StandardCharsets.UTF_8)) + val signed = comms.authenticate(pin1, encoded) // Return the signed authentication token. + return jwt + "." + base64Encode(signed) } + + fun base64Encode(bytes: ByteArray): String? { + val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes) + return encoded.replace("=", "") + } + } \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/model/ParametersViewModel.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/model/ParametersViewModel.kt index 0a76f1d..dbac034 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/model/ParametersViewModel.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/model/ParametersViewModel.kt @@ -13,6 +13,9 @@ class ParametersViewModel: ViewModel() { private var _token: String = "" val token get() = _token + private var _origin: String = "" + val origin get() = _origin + fun setChallenge(newChallenge: String) { _challenge = newChallenge } @@ -24,4 +27,8 @@ class ParametersViewModel: ViewModel() { fun setToken(newToken: String) { _token = newToken } + + fun setOrigin(newOrigin: String) { + _origin = newOrigin + } } \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt index 56ad1c1..67b952b 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/network/TokenApiService.kt @@ -14,8 +14,8 @@ import retrofit2.http.POST * Class for making HTTP requests * Based on https://developer.android.com/courses/pathways/android-basics-kotlin-unit-4-pathway-2 */ -private const val BASE_URL = - "add-endpoint-url-here" +const val BASE_URL = + "https://6bb0-85-253-195-252.ngrok.io" private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() private val retrofit = Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi)) @@ -23,8 +23,8 @@ private val retrofit = Retrofit.Builder().addConverterFactory(MoshiConverterFact interface TokenApiService { @Headers("Content-Type: application/json") - @POST("auth/authentication") - suspend fun postToken(@Body data: TokenItem): Response + @POST("/auth/authentication") + suspend fun postToken(@Body data: String): Response } object TokenApi { diff --git a/MobileAuthApp/app/src/main/res/values-en/strings.xml b/MobileAuthApp/app/src/main/res/values-en/strings.xml index 7aa4439..36dce82 100644 --- a/MobileAuthApp/app/src/main/res/values-en/strings.xml +++ b/MobileAuthApp/app/src/main/res/values-en/strings.xml @@ -56,8 +56,8 @@ FORGET - See Fragment vastutab vastuse tagastamise eest. - Hiljem sulgeb rakendus automaatselt. + Controlling the created token + Wait for the app to close Settings diff --git a/MobileAuthApp/app/src/main/res/values-et/strings.xml b/MobileAuthApp/app/src/main/res/values-et/strings.xml index 66afcfb..63c0eae 100644 --- a/MobileAuthApp/app/src/main/res/values-et/strings.xml +++ b/MobileAuthApp/app/src/main/res/values-et/strings.xml @@ -55,8 +55,8 @@ SUGU - See Fragment vastutab vastuse tagastamise eest. - Hiljem sulgeb rakendus automaatselt. + Tulemust kontrollitakse + Rakendus sulgeb ennast ise Seaded diff --git a/MobileAuthApp/app/src/main/res/values/strings.xml b/MobileAuthApp/app/src/main/res/values/strings.xml index 0a39206..9c2d2a7 100644 --- a/MobileAuthApp/app/src/main/res/values/strings.xml +++ b/MobileAuthApp/app/src/main/res/values/strings.xml @@ -55,8 +55,8 @@ FORGET - See Fragment vastutab vastuse tagastamise eest. - Hiljem sulgeb rakendus automaatselt. + Controlling the created token + Wait for the app to close Settings diff --git a/MobileAuthApp/build.gradle b/MobileAuthApp/build.gradle index 0e1f251..0367823 100644 --- a/MobileAuthApp/build.gradle +++ b/MobileAuthApp/build.gradle @@ -9,7 +9,7 @@ buildscript { kotlin_version = "1.4.30" } dependencies { - classpath "com.android.tools.build:gradle:7.0.2" + classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/README.md b/README.md index cccf57d..5d091a7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ More info about installing third party applications on the Android phones: https **NB! Before using the application make sure that the NFC is enabled on the phone, otherwise information can not be read from the ID card.** +### Testing the application +The project comes with a test mobile application and a test web application that can be used to try the MobileAuthApp authentication feature even if you don't have any web applications or mobile applications that require user authentication. Both projects come with a README file that help with a setup. + ### Wiki pages relevant for the "Software project" subject * [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision) *last updated on 10.10* * [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes) *last updated for iteration3 on 08.11* diff --git a/TestMobileApp/README.md b/TestMobileApp/README.md new file mode 100644 index 0000000..e84ab0d --- /dev/null +++ b/TestMobileApp/README.md @@ -0,0 +1,10 @@ +# TestMobileApp overview +### The purpose +The TestMobileApp was created in order to demonstrate how a different application on the Android smartphone could use the MobileAuthApp for user authentication purposes. +### Installing the application +The application installation process is the same as with the MobileAuthApp. Check the guide in the project's [main readme file](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC#installing-the-application-on-the-phone). +### Using the application +In order to use this application a backend server must be running that can issue challenges and verify the token created by the MobileAuthApp. +Use demoBackend application that is included in the project. Follow the demoBackend setup guide and once you have a backend running take the https address of the backend +and add it in the TestMobileApp's MainActivty.kt file as the new value for the constant variable BASE_URL (this is easly noticeable in the class as it is pointed out with a comment). +Now the app can be used. diff --git a/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt b/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt index 4184aad..afc6035 100644 --- a/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt +++ b/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt @@ -5,10 +5,18 @@ import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log +import android.view.View import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import com.example.testmobileapp.databinding.ActivityMainBinding +import com.google.gson.JsonObject import com.koushikdutta.ion.Ion +import org.json.JSONObject + +/** + * Base url where the requests should be made. Add yours here. It must use https. + */ +private const val BASE_URL = "https-base-url-here" /** * Test mobile app to demonstrate how other applications can use MobileAuthApp. @@ -18,9 +26,11 @@ class MainActivity : AppCompatActivity() { private lateinit var authLauncher: ActivityResultLauncher + private lateinit var binding: ActivityMainBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityMainBinding.inflate(layoutInflater) + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response -> @@ -28,25 +38,45 @@ class MainActivity : AppCompatActivity() { // Currently we are not actually checking whether we get a valid token. // For testing purposes only, to make sure that we are able to get a response at all. binding.loginTextView.text = getString(R.string.auth_success) + Log.i("getResult", response.data?.getStringExtra("token").toString()) + Log.i("getResult", response.data?.getStringExtra("result").toString()) + var user = "" + try { + val resultObject = JSONObject(response.data?.getStringExtra("result").toString()) + user = resultObject.getString("principal") + } catch (e: Exception) { + Log.i("getResult", "unable to retrieve name from principal") + } + showResult(user) + /* + binding.loginOptionNfcButton.text = "Log Out" + binding.loginOptionNfcButton.setOnClickListener { + binding.loginOptionNfcButton.text = "NFC auth" + binding.loginOptionNfcButton.setOnClickListener { getData() } + } + + */ } if (response.resultCode == Activity.RESULT_CANCELED) { binding.loginTextView.text = getString(R.string.auth_failure) } } - binding.loginOptionNfcButton.setOnClickListener { launchAuth() } - //binding.loginOptionNfcButton.setOnClickListener { getData() } + showLogin() + + binding.loginOptionNfcButton.setOnClickListener { getData() } } /** * Method that creates an intent to launch the MobileAuthApp */ - private fun launchAuth(challenge: String = "challenge", authUrl: String = "authUrl") { + private fun launchAuth(challenge: String = "challenge", originUrl: String = "baseUrl", authUrl: String = "authUrl") { val launchIntent = Intent() launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity") launchIntent.putExtra("action", "auth") launchIntent.putExtra("challenge", challenge) + launchIntent.putExtra("originUrl", originUrl) launchIntent.putExtra("authUrl", authUrl) launchIntent.putExtra("mobile", true) authLauncher.launch(launchIntent) @@ -58,19 +88,35 @@ class MainActivity : AppCompatActivity() { */ private fun getData() { // Enter the server endpoint address to here - val baseUrl = "enter-base-url-here" - val url = "$baseUrl/auth/challenge" + val url = "$BASE_URL/auth/challenge" + Ion.getDefault(this).conscryptMiddleware.enable(false) Ion.with(applicationContext) .load(url) .asJsonObject() .setCallback { _, result -> try { // Get data from the result and call launchAuth method - val challenge = result.asJsonObject["nonce"].toString() - launchAuth(challenge, baseUrl) + val challenge = result.asJsonObject["nonce"].toString().replace("\"", "") + Log.v("Challenge", challenge) + launchAuth(challenge, BASE_URL, "/auth/authentication") } catch (e: Exception) { Log.i("GETrequest", "was unsuccessful") } } } + + private fun showLogin() { + binding.loginOptions.visibility = View.VISIBLE + } + + private fun showResult(user: String) { + binding.loginOptions.visibility = View.GONE + binding.resultLayout.visibility = View.VISIBLE + binding.resultObject.text = getString(R.string.hello, user) + binding.buttonForget.setOnClickListener { + binding.resultObject.text = "" + binding.resultLayout.visibility = View.GONE + binding.loginOptions.visibility = View.VISIBLE + } + } } \ No newline at end of file diff --git a/TestMobileApp/app/src/main/res/layout/activity_main.xml b/TestMobileApp/app/src/main/res/layout/activity_main.xml index 33cefac..dab6a25 100644 --- a/TestMobileApp/app/src/main/res/layout/activity_main.xml +++ b/TestMobileApp/app/src/main/res/layout/activity_main.xml @@ -26,7 +26,8 @@ android:layout_margin="12dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/login_text_view" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + android:visibility="gone"> + + + + + + + + + \ No newline at end of file diff --git a/demoBackend/src/main/resources/templates/signature.html b/demoBackend/src/main/resources/templates/signature.html new file mode 100644 index 0000000..5c4bd12 --- /dev/null +++ b/demoBackend/src/main/resources/templates/signature.html @@ -0,0 +1,35 @@ + + + + Login + + + + + + + + + + +
+

Congratulations! You have just authenticated yourself using your mobile phone and your ID-card. You can try to + give a signature to a file now.

+
This page is still WIP, signing a document feature will be implemented later.
+
+ +
+ +
+ + \ No newline at end of file diff --git a/demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt b/demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt new file mode 100644 index 0000000..9e9ca3a --- /dev/null +++ b/demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt @@ -0,0 +1,13 @@ +package com.tarkvaratehnika.demobackend + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class DemoBackendApplicationTests { + + @Test + fun contextLoads() { + } + +}