From d2ad8920a11589ffecfa20927eff96f41e159094 Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Tue, 12 Oct 2021 00:34:06 +0300 Subject: [PATCH 01/18] Add a method for verifying PIN1. --- .../mobileauthapp/NFC/Comms.java | 130 +++++++++++++----- 1 file changed, 92 insertions(+), 38 deletions(-) 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..a6782d4 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.Locale; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -30,7 +31,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class Comms { - private static final byte[] master = { // select Main AID + private static final byte[] selectMain = { // select IAS-ECC 0, -92, 4, 12, 16, -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0 }; @@ -58,19 +59,31 @@ public class Comms { 127, 73, 79, 6, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -122, 65 }; - private static final byte[] masterSec = { + private static final byte[] selectAppSecure = { 12, -92, 4, 12, 45, -121, 33, 1 }; - private static final byte[] personal = { // select personal data DF + private static final byte[] IASECCAID = { // Identification Authentication Signature - European Citizen Card Application Identifier + -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0 + }; + + private static final byte[] QSCDAID = { // Qualified Signature Creation Device Application Identifier + 81, 83, 67, 68, 32, 65, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110 + }; + + private static final byte[] selectFile = { // 12, -92, 1, 12, 29, -121, 17, 1 }; - private static final byte[] read = { // read binary + private static final byte[] read = { 12, -80, 0, 0, 13, -105, 1, 0 }; - private IsoDep idCard; + private static final byte[] verifyPIN1 = { + 12, 32, 0, 1, 29, -121, 17, 1 + }; + + private final IsoDep idCard; private final byte[] keyEnc; private final byte[] keyMAC; private byte ssc; // Send sequence counter. @@ -95,10 +108,6 @@ public class Comms { keyMAC = keys[1]; } - public byte[] getAuthenticationCertificate() { - return new byte[0]; - } - /** * Calculates the message authentication code * @@ -165,8 +174,8 @@ public class Comms { */ private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { - // select the ECC applet on the chip - byte[] response = idCard.transceive(master); + // select the IAS-ECC application on the chip + byte[] response = idCard.transceive(selectMain); Log.i("Select applet", Hex.toHexString(response)); // initiate PACE @@ -221,6 +230,22 @@ public class Comms { } + /** + * Selects a file and reads its contents + * + * @param FID file identifier of the required file + * @return decrypted file contents + */ + private byte[] readFileByFID(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + byte [] APDU = createSecureAPDU(FID, selectFile); + byte [] response = idCard.transceive(APDU); + Log.i(String.format("Select FID %s", Hex.toHexString(FID)), Hex.toHexString(response)); + APDU = createSecureAPDU(new byte[0], read); + response = idCard.transceive(APDU); + Log.i("Read binary", Hex.toHexString(response)); + return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE); + } + /** * Encrypts or decrypts the APDU data * @@ -267,8 +292,7 @@ public class Comms { 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); } @@ -280,49 +304,79 @@ public class Comms { } + /** + * Selects the IAS ECC application, which provides the PKI functionalities, after a successful PACE. + */ + private void selectIASECCApplication() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + byte[] APDU = createSecureAPDU(IASECCAID, selectAppSecure); + byte[] response = idCard.transceive(APDU); + Log.i("Select the main application", Hex.toHexString(response)); + if (!Hex.toHexString(response, response.length - 2, 2).equals("9000")) { + throw new RuntimeException("Could not select IAS-ECC."); // *Should* never happen. + } + } + /** * 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 + * @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[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + public String[] readPersonalData(byte[] lastBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { - String[] personalData = new String[FID.length]; - byte[] data; - byte[] APDU; - byte[] response; + String[] personalData = new String[lastBytes.length]; + int stringIndex = 0; + + // select the application + selectIASECCApplication(); // select the personal data dedicated file - data = new byte[]{80, 0}; // personal data DF FID - APDU = createSecureAPDU(data, personal); - response = idCard.transceive(APDU); + byte[] FID = new byte[]{80, 0}; // personal data dedicated file FID + byte[] APDU = createSecureAPDU(FID, selectFile); + byte[] response = idCard.transceive(APDU); Log.i("Select personal data DF", Hex.toHexString(response)); - // select and read the first 8 elementary files in the DF - for (int i = 0; i < FID.length; i++) { + // 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)); + response = readFileByFID(FID); + int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2; + personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator)); } return personalData; } + + /** + * Attempts to verify the user-provided PIN1 + * + * @param PIN1 byte array containing the PIN1 + */ + private void verifyPIN1(byte[] PIN1) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + selectIASECCApplication(); + byte[] paddedPIN1 = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + System.arraycopy(PIN1, 0, paddedPIN1, 0, PIN1.length); + byte[] APDU = createSecureAPDU(paddedPIN1, verifyPIN1); + byte[] response = idCard.transceive(APDU); + Log.i("PIN1 verification", Hex.toHexString(response)); + String sw1sw2 = Hex.toHexString(response, response.length - 2, 2); + if (!sw1sw2.equals("9000")) { + if (sw1sw2.equals("6983")) throw new RuntimeException("Invalid PIN1. Authentication method blocked."); + else throw new RuntimeException(String.format("Invalid PIN1. %c attempt%s left.", sw1sw2.charAt(sw1sw2.length() - 1), sw1sw2.endsWith("1") ? "" : "s")); + } + } + + public byte[] getAuthenticationCertificate(String PIN1) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + + return new byte[0]; + + } + } From 1c8a606376a1b3c80f1be9ab36a10b1237de49fc Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Tue, 12 Oct 2021 00:36:08 +0300 Subject: [PATCH 02/18] Add a method for getting the authentication certificate (WIP). --- .../mobileauthapp/NFC/Comms.java | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) 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 a6782d4..da09127 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 @@ -373,10 +373,45 @@ public class Comms { } } - public byte[] getAuthenticationCertificate(String PIN1) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + /** + * Retrieves the authentication certificate from the chip + * + * @return authentication certificate + */ + public byte[] getAuthenticationCertificate() throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { - return new byte[0]; + selectIASECCApplication(); + + byte[] APDU = createSecureAPDU(new byte[]{-83, -15}, selectFile); + byte[] response = idCard.transceive(APDU); + Log.i("Select AWP Application", Hex.toHexString(response)); + + APDU = createSecureAPDU(new byte[]{52, 1}, selectFile); + response = idCard.transceive(APDU); + Log.i("Select certificate", Hex.toHexString(response)); + + byte[] responses = new byte[0]; + byte[] readCert = Arrays.copyOf(read, read.length); + for (int i = 0; i < 5; i++) { + + readCert[2] = (byte) i; + APDU = createSecureAPDU(new byte[0], readCert); + response = idCard.transceive(APDU); + Log.i("Read certificate", Hex.toHexString(response)); + + if (!Hex.toHexString(response).substring(response.length * 2 - 4).equals("6b00")) { + byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, 4, 244), Cipher.DECRYPT_MODE); + responses = Arrays.copyOf(responses, responses.length + decrypted.length); + System.arraycopy(decrypted, 0, responses, responses.length - decrypted.length, decrypted.length); + } else { + break; + } + + } + + Log.i("Certificate", new String(responses, StandardCharsets.UTF_8)); + + return responses; } - } From 25c01803cb1f043834fba71fc4deac9a977eac78 Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Tue, 12 Oct 2021 03:04:00 +0300 Subject: [PATCH 03/18] auth cert bug fix progress --- .../mobileauthapp/NFC/Comms.java | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) 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 da09127..3c65287 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 @@ -380,38 +380,7 @@ public class Comms { */ public byte[] getAuthenticationCertificate() throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { - selectIASECCApplication(); - - byte[] APDU = createSecureAPDU(new byte[]{-83, -15}, selectFile); - byte[] response = idCard.transceive(APDU); - Log.i("Select AWP Application", Hex.toHexString(response)); - - APDU = createSecureAPDU(new byte[]{52, 1}, selectFile); - response = idCard.transceive(APDU); - Log.i("Select certificate", Hex.toHexString(response)); - - byte[] responses = new byte[0]; - byte[] readCert = Arrays.copyOf(read, read.length); - for (int i = 0; i < 5; i++) { - - readCert[2] = (byte) i; - APDU = createSecureAPDU(new byte[0], readCert); - response = idCard.transceive(APDU); - Log.i("Read certificate", Hex.toHexString(response)); - - if (!Hex.toHexString(response).substring(response.length * 2 - 4).equals("6b00")) { - byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, 4, 244), Cipher.DECRYPT_MODE); - responses = Arrays.copyOf(responses, responses.length + decrypted.length); - System.arraycopy(decrypted, 0, responses, responses.length - decrypted.length, decrypted.length); - } else { - break; - } - - } - - Log.i("Certificate", new String(responses, StandardCharsets.UTF_8)); - - return responses; + return new byte[0]; } } From 9c48cc9c1a523ce4d3ba7a91663ab0a9747a83d9 Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Tue, 12 Oct 2021 12:18:06 +0300 Subject: [PATCH 04/18] Fix authentication certificate retrieval. --- .../mobileauthapp/NFC/Comms.java | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) 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 3c65287..7ddde80 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 @@ -12,6 +12,7 @@ import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.util.encoders.Hex; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; @@ -20,6 +21,9 @@ import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Locale; @@ -378,9 +382,54 @@ public class Comms { * * @return authentication certificate */ - public byte[] getAuthenticationCertificate() throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + public byte[] getAuthenticationCertificate() throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException, CertificateException { - return new byte[0]; + selectIASECCApplication(); + + byte[] APDU = createSecureAPDU(new byte[]{-83, -15}, selectFile); + byte[] response = idCard.transceive(APDU); + Log.i("Select AWP Application", Hex.toHexString(response)); + + APDU = createSecureAPDU(new byte[]{52, 1}, selectFile); + response = idCard.transceive(APDU); + Log.i("Select certificate", Hex.toHexString(response)); + + byte[] responses = new byte[0]; + byte[] readCert = Arrays.copyOf(read, read.length); + int indexOfTerminator = 0; + for (int i = 0; i < 9; i++) { + + readCert[2] = (byte) ((byte) i / 2); + readCert[3] = (byte) ((byte) (i % 2) * 25); + APDU = createSecureAPDU(new byte[0], readCert); + response = idCard.transceive(APDU); + Log.i("Read certificate part " + i, Hex.toHexString(response)); + + if (!Hex.toHexString(response).substring(response.length * 2 - 4).equals("6b00")) { + byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, 4, 244), Cipher.DECRYPT_MODE); + if (i % 2 == 0) { + indexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2; + responses = Arrays.copyOf(responses, responses.length + indexOfTerminator); + System.arraycopy(decrypted, 0, responses, responses.length - indexOfTerminator, indexOfTerminator); +// Log.i("Partial certificate #1", new String(Arrays.copyOf(decrypted, indexOfTerminator), StandardCharsets.ISO_8859_1)); + } else { + int newIndexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2; + responses = Arrays.copyOf(responses, responses.length + 25 - indexOfTerminator + newIndexOfTerminator); + System.arraycopy(decrypted, 0, responses, responses.length - newIndexOfTerminator, newIndexOfTerminator); +// Log.i("Partial certificate #2", new String(Arrays.copyOfRange(decrypted, newIndexOfTerminator - 25, newIndexOfTerminator), StandardCharsets.ISO_8859_1)); + } + } else { + break; + } + + } + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(responses)); + + Log.i("Certificate subject", certificate.getSubjectX500Principal().getName()); + + return responses; } } From 29c7ecfa12fb4865526f71e7cea00d6dbfb55f66 Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Wed, 13 Oct 2021 02:27:19 +0300 Subject: [PATCH 05/18] Refactor the method for authentication certificate retrieval. --- .../mobileauthapp/NFC/Comms.java | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) 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 7ddde80..f548f93 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 @@ -12,7 +12,6 @@ import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.util.encoders.Hex; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; @@ -22,10 +21,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.Locale; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -101,13 +97,8 @@ 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)); - keyEnc = keys[0]; keyMAC = keys[1]; } @@ -228,7 +219,7 @@ public class Comms { APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65); MAC = getMAC(APDU, 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}; @@ -302,7 +293,6 @@ public class Comms { } 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); - ssc++; return APDU; @@ -325,7 +315,6 @@ public class Comms { * * @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 { @@ -353,7 +342,6 @@ public class Comms { personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator)); } - return personalData; } @@ -364,16 +352,23 @@ public class Comms { * @param PIN1 byte array containing the PIN1 */ private void verifyPIN1(byte[] PIN1) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + selectIASECCApplication(); + byte[] paddedPIN1 = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; System.arraycopy(PIN1, 0, paddedPIN1, 0, PIN1.length); byte[] APDU = createSecureAPDU(paddedPIN1, verifyPIN1); byte[] response = idCard.transceive(APDU); Log.i("PIN1 verification", Hex.toHexString(response)); - String sw1sw2 = Hex.toHexString(response, response.length - 2, 2); - if (!sw1sw2.equals("9000")) { - if (sw1sw2.equals("6983")) throw new RuntimeException("Invalid PIN1. Authentication method blocked."); - else throw new RuntimeException(String.format("Invalid PIN1. %c attempt%s left.", sw1sw2.charAt(sw1sw2.length() - 1), sw1sw2.endsWith("1") ? "" : "s")); + + byte sw1 = response[response.length - 2]; + byte sw2 = response[response.length - 1]; + if (sw1 != -112 || sw2 != 0) { + if (sw1 == 105 && sw2 == -125) { + throw new RuntimeException("Invalid PIN1. Authentication method blocked."); + } else { + throw new RuntimeException(String.format("Invalid PIN1. %d attempt%s left.", sw2 + 64, sw2 == -63 ? "" : "s")); + } } } @@ -388,48 +383,49 @@ public class Comms { byte[] APDU = createSecureAPDU(new byte[]{-83, -15}, selectFile); byte[] response = idCard.transceive(APDU); - Log.i("Select AWP Application", Hex.toHexString(response)); + Log.i("Select AWP application", Hex.toHexString(response)); APDU = createSecureAPDU(new byte[]{52, 1}, selectFile); response = idCard.transceive(APDU); Log.i("Select certificate", Hex.toHexString(response)); - byte[] responses = new byte[0]; + byte[] certificate = new byte[0]; byte[] readCert = Arrays.copyOf(read, read.length); - int indexOfTerminator = 0; - for (int i = 0; i < 9; i++) { + // Construct the certificate byte array n=indexOfTerminator bytes at a time + for (int i = 0; i < 16; i++) { - readCert[2] = (byte) ((byte) i / 2); - readCert[3] = (byte) ((byte) (i % 2) * 25); + // Set the P1/P2 values to incrementally read the certificate + readCert[2] = (byte) (certificate.length / 256); + readCert[3] = (byte) (certificate.length % 256); APDU = createSecureAPDU(new byte[0], readCert); response = idCard.transceive(APDU); - Log.i("Read certificate part " + i, Hex.toHexString(response)); + Log.i("Read certificate", Hex.toHexString(response)); - if (!Hex.toHexString(response).substring(response.length * 2 - 4).equals("6b00")) { - byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, 4, 244), Cipher.DECRYPT_MODE); - if (i % 2 == 0) { - indexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2; - responses = Arrays.copyOf(responses, responses.length + indexOfTerminator); - System.arraycopy(decrypted, 0, responses, responses.length - indexOfTerminator, indexOfTerminator); -// Log.i("Partial certificate #1", new String(Arrays.copyOf(decrypted, indexOfTerminator), StandardCharsets.ISO_8859_1)); - } else { - int newIndexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2; - responses = Arrays.copyOf(responses, responses.length + 25 - indexOfTerminator + newIndexOfTerminator); - System.arraycopy(decrypted, 0, responses, responses.length - newIndexOfTerminator, newIndexOfTerminator); -// Log.i("Partial certificate #2", new String(Arrays.copyOfRange(decrypted, newIndexOfTerminator - 25, newIndexOfTerminator), StandardCharsets.ISO_8859_1)); - } - } else { - break; + byte sw1 = response[response.length - 2]; + byte sw2 = response[response.length - 1]; + if (sw1 == 107 && sw2 == 0) { + 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 (sw1 == -112 && sw2 == 0) { + break; + } } - CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); - X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(responses)); + // For debugging, ascertain that the byte array corresponds to a valid certificate +// CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); +// X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); +// Log.i("Certificate subject", x509Certificate.getSubjectX500Principal().getName()); - Log.i("Certificate subject", certificate.getSubjectX500Principal().getName()); - - return responses; + return certificate; } } From ef7015abb804928003decb138660d7be6298c2d8 Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Thu, 14 Oct 2021 03:58:49 +0300 Subject: [PATCH 06/18] Refactor (generalise selecting and reading a file, PIN verification and certificate retrieval). --- .../mobileauthapp/NFC/Comms.java | 110 ++++++++++-------- .../mobileauthapp/auth/Authenticator.kt | 2 +- MobileAuthApp/build.gradle | 2 +- 3 files changed, 62 insertions(+), 52 deletions(-) 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 f548f93..5fe496f 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 @@ -20,7 +20,6 @@ import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.security.cert.CertificateException; import java.util.Arrays; import javax.crypto.BadPaddingException; @@ -31,7 +30,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class Comms { - private static final byte[] selectMain = { // select IAS-ECC + private static final byte[] selectMaster = { // select IAS-ECC 0, -92, 4, 12, 16, -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0 }; @@ -59,7 +58,7 @@ public class Comms { 127, 73, 79, 6, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -122, 65 }; - private static final byte[] selectAppSecure = { + private static final byte[] selectMasterSecure = { 12, -92, 4, 12, 45, -121, 33, 1 }; @@ -67,10 +66,6 @@ public class Comms { -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0 }; - private static final byte[] QSCDAID = { // Qualified Signature Creation Device Application Identifier - 81, 83, 67, 68, 32, 65, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110 - }; - private static final byte[] selectFile = { // 12, -92, 1, 12, 29, -121, 17, 1 }; @@ -83,6 +78,18 @@ public class Comms { 12, 32, 0, 1, 29, -121, 17, 1 }; + private static final byte[] verifyPIN2 = { + 12, 32, 0, -123, 29, -121, 17, 1 + }; + + private static final byte[] IASECCFID = {63, 0}; + private static final byte[] personalDF = {80, 0}; + private static final byte[] AWP = {-83, -15}; + private static final byte[] QSCD = {-83, -14}; + private static final byte[] authCert = {52, 1}; + private static final byte[] signCert = {52, 31}; + + private final IsoDep idCard; private final byte[] keyEnc; private final byte[] keyMAC; @@ -170,8 +177,8 @@ public class Comms { private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { // select the IAS-ECC application on the chip - byte[] response = idCard.transceive(selectMain); - Log.i("Select applet", Hex.toHexString(response)); + byte[] response = idCard.transceive(selectMaster); + Log.i("Select the master application", Hex.toHexString(response)); // initiate PACE response = idCard.transceive(MSESetAT); @@ -229,15 +236,17 @@ public class Comms { * 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[] readFileByFID(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { - byte [] APDU = createSecureAPDU(FID, selectFile); - byte [] response = idCard.transceive(APDU); - Log.i(String.format("Select FID %s", Hex.toHexString(FID)), Hex.toHexString(response)); - APDU = createSecureAPDU(new byte[0], read); - response = idCard.transceive(APDU); + private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + selectFile(FID, info); + byte[] APDU = createSecureAPDU(new byte[0], read); + byte[] response = idCard.transceive(APDU); Log.i("Read binary", Hex.toHexString(response)); + if (response[response.length - 2] != -112 || response[response.length - 1] != 0) { + throw new RuntimeException(String.format("Could not read %s", info)); + } return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE); } @@ -299,14 +308,15 @@ public class Comms { } /** - * Selects the IAS ECC application, which provides the PKI functionalities, after a successful PACE. + * Selects a FILE by its identifier + * */ - private void selectIASECCApplication() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { - byte[] APDU = createSecureAPDU(IASECCAID, selectAppSecure); + private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { + byte[] APDU = createSecureAPDU(FID, selectFile); byte[] response = idCard.transceive(APDU); - Log.i("Select the main application", Hex.toHexString(response)); - if (!Hex.toHexString(response, response.length - 2, 2).equals("9000")) { - throw new RuntimeException("Could not select IAS-ECC."); // *Should* never happen. + Log.i(String.format("Select %s", info), Hex.toHexString(response)); + if (response[response.length - 2] != -112 || response[response.length - 1] != 0) { + throw new RuntimeException(String.format("Could not select %s", info)); } } @@ -321,15 +331,13 @@ public class Comms { String[] personalData = new String[lastBytes.length]; int stringIndex = 0; - // select the application - selectIASECCApplication(); + // select the master application + selectFile(IASECCFID, "the master application"); // select the personal data dedicated file - byte[] FID = new byte[]{80, 0}; // personal data dedicated file FID - byte[] APDU = createSecureAPDU(FID, selectFile); - byte[] response = idCard.transceive(APDU); - Log.i("Select personal data DF", Hex.toHexString(response)); + selectFile(personalDF, "the personal data DF"); + byte[] FID = Arrays.copyOf(personalDF, personalDF.length); // select and read the personal data elementary files for (byte index : lastBytes) { @@ -337,7 +345,7 @@ public class Comms { FID[1] = index; // store the decrypted datum - response = readFileByFID(FID); + 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)); @@ -347,25 +355,30 @@ public class Comms { } /** - * Attempts to verify the user-provided PIN1 + * Attempts to verify the selected PIN * - * @param PIN1 byte array containing the PIN1 + * @param PIN user-provided PIN + * @param oneOrTwo true for PIN1, false for PIN2 */ - private void verifyPIN1(byte[] PIN1) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + public void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { - selectIASECCApplication(); + selectFile(IASECCFID, "the master application"); + if (!oneOrTwo) { + selectFile(QSCD, "the application"); + } + // pad the PIN and use the chip for verification byte[] paddedPIN1 = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - System.arraycopy(PIN1, 0, paddedPIN1, 0, PIN1.length); - byte[] APDU = createSecureAPDU(paddedPIN1, verifyPIN1); + System.arraycopy(PIN, 0, paddedPIN1, 0, PIN.length); + byte[] APDU = createSecureAPDU(paddedPIN1, oneOrTwo ? verifyPIN1 : verifyPIN2); byte[] response = idCard.transceive(APDU); - Log.i("PIN1 verification", Hex.toHexString(response)); + Log.i(String.format("PIN%d verification", oneOrTwo ? 1 : 2), Hex.toHexString(response)); byte sw1 = response[response.length - 2]; byte sw2 = response[response.length - 1]; if (sw1 != -112 || sw2 != 0) { if (sw1 == 105 && sw2 == -125) { - throw new RuntimeException("Invalid PIN1. Authentication method blocked."); + throw new RuntimeException("Invalid PIN. Authentication method blocked."); } else { throw new RuntimeException(String.format("Invalid PIN1. %d attempt%s left.", sw2 + 64, sw2 == -63 ? "" : "s")); } @@ -373,21 +386,18 @@ public class Comms { } /** - * Retrieves the authentication certificate from the chip + * Retrieves the authentication or signature certificate from the chip * - * @return authentication certificate + * @param authOrSign true for auth, false for sign cert + * @return the requested certificate */ - public byte[] getAuthenticationCertificate() throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException, CertificateException { + public byte[] getCertificate(boolean authOrSign) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { - selectIASECCApplication(); + selectFile(IASECCFID, "the master application"); - byte[] APDU = createSecureAPDU(new byte[]{-83, -15}, selectFile); - byte[] response = idCard.transceive(APDU); - Log.i("Select AWP application", Hex.toHexString(response)); + selectFile(authOrSign ? AWP : QSCD, "the application"); - APDU = createSecureAPDU(new byte[]{52, 1}, selectFile); - response = idCard.transceive(APDU); - Log.i("Select certificate", Hex.toHexString(response)); + selectFile(authOrSign ? authCert : signCert, "the certificate"); byte[] certificate = new byte[0]; byte[] readCert = Arrays.copyOf(read, read.length); @@ -397,9 +407,9 @@ public class Comms { // Set the P1/P2 values to incrementally read the certificate readCert[2] = (byte) (certificate.length / 256); readCert[3] = (byte) (certificate.length % 256); - APDU = createSecureAPDU(new byte[0], readCert); - response = idCard.transceive(APDU); - Log.i("Read certificate", Hex.toHexString(response)); + byte[] APDU = createSecureAPDU(new byte[0], readCert); + byte[] response = idCard.transceive(APDU); + Log.i("Read the certificate", Hex.toHexString(response)); byte sw1 = response[response.length - 2]; byte sw2 = response[response.length - 1]; @@ -423,7 +433,7 @@ public class Comms { // For debugging, ascertain that the byte array corresponds to a valid certificate // CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); // X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); -// Log.i("Certificate subject", x509Certificate.getSubjectX500Principal().getName()); +// Log.i("Certificate serial number", String.valueOf(x509Certificate.getSerialNumber())); return certificate; 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..e2f86fd 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 @@ -9,7 +9,7 @@ class Authenticator(val comms : Comms) { public fun authenticate(nonce: BigInteger, challengeUrl: String, pin1: 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) 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 From 850ab8fc660afb6015dcc3fa7ad9e9cd48b44026 Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Thu, 14 Oct 2021 19:50:53 +0300 Subject: [PATCH 07/18] Use hex to represent bytes. --- .../mobileauthapp/NFC/Comms.java | 72 ++++++------------- 1 file changed, 21 insertions(+), 51 deletions(-) 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 5fe496f..39b04bf 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 @@ -30,65 +30,35 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class Comms { - private static final byte[] selectMaster = { // select IAS-ECC - 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[] selectMasterSecure = { - 12, -92, 4, 12, 45, -121, 33, 1 - }; + private static final byte[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641"); - private static final byte[] IASECCAID = { // Identification Authentication Signature - European Citizen Card Application Identifier - -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0 - }; + private static final byte[] selectFile = Hex.decode("0ca4010c1d871101"); - private static final byte[] selectFile = { // - 12, -92, 1, 12, 29, -121, 17, 1 - }; + private static final byte[] readFile = Hex.decode("0cb000000d970100"); - private static final byte[] read = { - 12, -80, 0, 0, 13, -105, 1, 0 - }; + private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101"); - private static final byte[] verifyPIN1 = { - 12, 32, 0, 1, 29, -121, 17, 1 - }; - - private static final byte[] verifyPIN2 = { - 12, 32, 0, -123, 29, -121, 17, 1 - }; - - private static final byte[] IASECCFID = {63, 0}; - private static final byte[] personalDF = {80, 0}; - private static final byte[] AWP = {-83, -15}; - private static final byte[] QSCD = {-83, -14}; - private static final byte[] authCert = {52, 1}; - private static final byte[] signCert = {52, 31}; + private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101"); + 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; @@ -241,7 +211,7 @@ public class Comms { */ private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { selectFile(FID, info); - byte[] APDU = createSecureAPDU(new byte[0], read); + byte[] APDU = createSecureAPDU(new byte[0], readFile); byte[] response = idCard.transceive(APDU); Log.i("Read binary", Hex.toHexString(response)); if (response[response.length - 2] != -112 || response[response.length - 1] != 0) { @@ -300,7 +270,7 @@ public class Comms { 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; @@ -368,7 +338,7 @@ public class Comms { } // pad the PIN and use the chip for verification - byte[] paddedPIN1 = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + byte[] paddedPIN1 = Hex.decode("ffffffffffffffffffffffff"); System.arraycopy(PIN, 0, paddedPIN1, 0, PIN.length); byte[] APDU = createSecureAPDU(paddedPIN1, oneOrTwo ? verifyPIN1 : verifyPIN2); byte[] response = idCard.transceive(APDU); @@ -400,7 +370,7 @@ public class Comms { selectFile(authOrSign ? authCert : signCert, "the certificate"); byte[] certificate = new byte[0]; - byte[] readCert = Arrays.copyOf(read, read.length); + 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++) { From 62888a72997fa5c2ca66ef49b4595365ed12e4ad Mon Sep 17 00:00:00 2001 From: Lemmo Lavonen Date: Tue, 19 Oct 2021 00:58:53 +0300 Subject: [PATCH 08/18] Add a method for signing the auth token hash. --- .../mobileauthapp/NFC/Comms.java | 159 ++++++++++-------- 1 file changed, 93 insertions(+), 66 deletions(-) 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 39b04bf..928312b 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 @@ -53,6 +53,12 @@ public class Comms { 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}; @@ -75,7 +81,7 @@ public class Comms { idCard.connect(); this.idCard = idCard; - byte[][] keys = PACE(CAN); + byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8)); keyEnc = keys[0]; keyMAC = keys[1]; } @@ -132,40 +138,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 IAS-ECC application on the chip - byte[] response = idCard.transceive(selectMaster); - Log.i("Select the master application", Hex.toHexString(response)); + 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 @@ -173,28 +190,18 @@ 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. } @@ -211,10 +218,8 @@ public class Comms { */ private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { selectFile(FID, info); - byte[] APDU = createSecureAPDU(new byte[0], readFile); - byte[] response = idCard.transceive(APDU); - Log.i("Read binary", Hex.toHexString(response)); - if (response[response.length - 2] != -112 || response[response.length - 1] != 0) { + 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); @@ -254,15 +259,15 @@ 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 @@ -282,10 +287,8 @@ public class Comms { * */ private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { - byte[] APDU = createSecureAPDU(FID, selectFile); - byte[] response = idCard.transceive(APDU); - Log.i(String.format("Select %s", info), Hex.toHexString(response)); - if (response[response.length - 2] != -112 || response[response.length - 1] != 0) { + 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)); } } @@ -330,7 +333,7 @@ public class Comms { * @param PIN user-provided PIN * @param oneOrTwo true for PIN1, false for PIN2 */ - public void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { + private void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { selectFile(IASECCFID, "the master application"); if (!oneOrTwo) { @@ -338,19 +341,15 @@ public class Comms { } // pad the PIN and use the chip for verification - byte[] paddedPIN1 = Hex.decode("ffffffffffffffffffffffff"); - System.arraycopy(PIN, 0, paddedPIN1, 0, PIN.length); - byte[] APDU = createSecureAPDU(paddedPIN1, oneOrTwo ? verifyPIN1 : verifyPIN2); - byte[] response = idCard.transceive(APDU); - Log.i(String.format("PIN%d verification", oneOrTwo ? 1 : 2), Hex.toHexString(response)); + byte[] paddedPIN = Hex.decode("ffffffffffffffffffffffff"); + System.arraycopy(PIN, 0, paddedPIN, 0, PIN.length); + byte[] response = getResponse(paddedPIN, oneOrTwo ? verifyPIN1 : verifyPIN2, "PIN verification"); - byte sw1 = response[response.length - 2]; - byte sw2 = response[response.length - 1]; - if (sw1 != -112 || sw2 != 0) { - if (sw1 == 105 && sw2 == -125) { + 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 PIN1. %d attempt%s left.", sw2 + 64, sw2 == -63 ? "" : "s")); + throw new RuntimeException(String.format("Invalid PIN. Attempts left: %d.", response[response.length - 1] + 64)); } } } @@ -377,13 +376,8 @@ public class Comms { // Set the P1/P2 values to incrementally read the certificate readCert[2] = (byte) (certificate.length / 256); readCert[3] = (byte) (certificate.length % 256); - byte[] APDU = createSecureAPDU(new byte[0], readCert); - byte[] response = idCard.transceive(APDU); - Log.i("Read the certificate", Hex.toHexString(response)); - - byte sw1 = response[response.length - 2]; - byte sw2 = response[response.length - 1]; - if (sw1 == 107 && sw2 == 0) { + 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."); } @@ -395,17 +389,50 @@ public class Comms { certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator); System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator); - if (sw1 == -112 && sw2 == 0) { + if (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == 0x00) { break; } } - // For debugging, ascertain that the byte array corresponds to a valid certificate -// CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); -// X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); -// Log.i("Certificate serial number", String.valueOf(x509Certificate.getSerialNumber())); - 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; + } + } From f9cd30922e2871b194d68a09a1a2ed97d23be747 Mon Sep 17 00:00:00 2001 From: TanelOrumaa Date: Mon, 8 Nov 2021 17:30:56 +0200 Subject: [PATCH 09/18] MOB-42 Added backend server, two frontend webpages and rest endpoints for getting challenge, submitting authentication token and getting authentication object. MOB-21 Added JWT creation, but whole process still needs some work. --- MobileAuthApp/app/build.gradle | 5 + .../mobileauthapp/AuthFragment.kt | 1 + .../mobileauthapp/NFC/Comms.java | 2 + .../mobileauthapp/auth/Authenticator.kt | 49 ++- demoBackend/.gitignore | 33 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 118 +++++++ demoBackend/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + demoBackend/mvnw | 310 ++++++++++++++++++ demoBackend/mvnw.cmd | 182 ++++++++++ demoBackend/pom.xml | 116 +++++++ .../demobackend/DemoBackendApplication.kt | 13 + .../config/ApplicationConfiguration.kt | 16 + .../config/ValidationConfiguration.kt | 152 +++++++++ .../demobackend/dto/ChallengeDto.kt | 3 + .../demobackend/security/AuthTokenDTO.kt | 6 + .../AuthTokenDTOAuthenticationProvider.kt | 71 ++++ .../security/WebEidAuthentication.kt | 100 ++++++ .../demobackend/web/LoginController.kt | 20 ++ .../demobackend/web/SignatureController.kt | 20 ++ .../web/rest/AuthenticationController.kt | 38 +++ .../web/rest/ChallengeController.kt | 45 +++ .../src/main/resources/application.properties | 1 + .../main/resources/certs/ESTEID-SK_2015.cer | Bin 0 -> 1652 bytes .../src/main/resources/certs/ESTEID2018.cer | Bin 0 -> 1371 bytes .../resources/certs/trusted_certificates.jks | Bin 0 -> 1345 bytes .../src/main/resources/static/css/main.css | 20 ++ .../src/main/resources/static/js/index.js | 14 + .../src/main/resources/static/js/main.js | 67 ++++ .../src/main/resources/templates/index.html | 38 +++ .../main/resources/templates/signature.html | 42 +++ .../DemoBackendApplicationTests.kt | 13 + 32 files changed, 1492 insertions(+), 5 deletions(-) create mode 100644 demoBackend/.gitignore create mode 100644 demoBackend/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 demoBackend/.mvn/wrapper/maven-wrapper.jar create mode 100644 demoBackend/.mvn/wrapper/maven-wrapper.properties create mode 100644 demoBackend/mvnw create mode 100644 demoBackend/mvnw.cmd create mode 100644 demoBackend/pom.xml create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt create mode 100644 demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt create mode 100644 demoBackend/src/main/resources/application.properties create mode 100644 demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer create mode 100644 demoBackend/src/main/resources/certs/ESTEID2018.cer create mode 100644 demoBackend/src/main/resources/certs/trusted_certificates.jks create mode 100644 demoBackend/src/main/resources/static/css/main.css create mode 100644 demoBackend/src/main/resources/static/js/index.js create mode 100644 demoBackend/src/main/resources/static/js/main.js create mode 100644 demoBackend/src/main/resources/templates/index.html create mode 100644 demoBackend/src/main/resources/templates/signature.html create mode 100644 demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt diff --git a/MobileAuthApp/app/build.gradle b/MobileAuthApp/app/build.gradle index 5cd7f6a..0d59bc9 100644 --- a/MobileAuthApp/app/build.gradle +++ b/MobileAuthApp/app/build.gradle @@ -62,4 +62,9 @@ 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' + } \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt index ab14a76..6cf616e 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.tarkvaraprojekt.mobileauthapp.NFC.Comms +import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import java.lang.Exception 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 928312b..76e1c3a 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 @@ -419,6 +419,7 @@ public class Comms { 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."); } @@ -429,6 +430,7 @@ public class Comms { 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)); 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 e2f86fd..0d7c862 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,61 @@ package com.tarkvaraprojekt.mobileauthapp.auth -import android.nfc.tech.IsoDep +import android.os.Message +import android.util.Log import com.tarkvaraprojekt.mobileauthapp.NFC.Comms -import java.math.BigInteger +import io.jsonwebtoken.SignatureAlgorithm +import org.bouncycastle.util.encoders.Base64 +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.time.LocalDateTime +import java.time.ZoneOffset +import javax.crypto.Mac +import kotlin.experimental.and 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.getCertificate(true); - // Create the authentication token (OpenID X509) + // Encode the certificate in base64. + val base64cert = String(Base64.encode(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() + + // Get subject value. + val sub = authenticationCertificate[0] // TODO: + + // 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":""}}""" + + var jwt = String(Base64.encode(header.toByteArray(Charsets.UTF_8))) + "." + String(Base64.encode(claims.toByteArray(Charsets.UTF_8))) + jwt = jwt.replace("=", "") + + Log.v("JWT", jwt) // 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()) + val signed = comms.authenticate(pin1, encoded) + + val jws = jwt + "." + String(Base64.encode(signed)) + + Log.v("Token", jws) // Return the signed authentication token. + return jws } + + } \ No newline at end of file diff --git a/demoBackend/.gitignore b/demoBackend/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/demoBackend/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/demoBackend/.mvn/wrapper/MavenWrapperDownloader.java b/demoBackend/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/demoBackend/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/demoBackend/.mvn/wrapper/maven-wrapper.jar b/demoBackend/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/demoBackend/.mvn/wrapper/maven-wrapper.properties b/demoBackend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..a9f1ef8 --- /dev/null +++ b/demoBackend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/demoBackend/mvnw b/demoBackend/mvnw new file mode 100644 index 0000000..a16b543 --- /dev/null +++ b/demoBackend/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/demoBackend/mvnw.cmd b/demoBackend/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/demoBackend/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/demoBackend/pom.xml b/demoBackend/pom.xml new file mode 100644 index 0000000..bd4ba2a --- /dev/null +++ b/demoBackend/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.6 + + + com.tarkvaratehnika + demoBackend + 0.0.1-SNAPSHOT + demoBackend + demoBackend + + 11 + 1.5.31 + 2.8.5 + 1.1.1 + + + + org.springframework.boot + spring-boot-starter-web + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.webeid.security + authtoken-validation + 1.2.0 + + + javax.cache + cache-api + ${javaxcache.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.github.ben-manes.caffeine + jcache + ${caffeine.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-config + + + + + + gitlab + https://gitlab.com/api/v4/projects/19948337/packages/maven + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt new file mode 100644 index 0000000..271a50a --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt @@ -0,0 +1,13 @@ +package com.tarkvaratehnika.demobackend + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.boot.runApplication + +@SpringBootApplication(exclude=[SecurityAutoConfiguration::class]) +class DemoBackendApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt new file mode 100644 index 0000000..d025eed --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt @@ -0,0 +1,16 @@ +package com.tarkvaratehnika.demobackend.config + +class ApplicationConfiguration { + + companion object { + // URL for intent, do not edit. + val AUTH_APP_LAUNCH_INTENT = "authapp://start/" + // Endpoint for challenge. + val CHALLENGE_ENDPOINT_URL = "/auth/challenge" + // Endpoint for authentication + val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication" + // URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here. + val WEBSITE_ORIGIN_URL = "https://6bb0-85-253-195-252.ngrok.io" + } + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt new file mode 100644 index 0000000..237fcdd --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt @@ -0,0 +1,152 @@ +package com.tarkvaratehnika.demobackend.config + +import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.support.PathMatchingResourcePatternResolver +import org.webeid.security.exceptions.JceException +import org.webeid.security.nonce.NonceGenerator +import org.webeid.security.nonce.NonceGeneratorBuilder +import org.webeid.security.validator.AuthTokenValidator +import org.webeid.security.validator.AuthTokenValidatorBuilder +import java.io.IOException +import java.net.URI +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.time.ZonedDateTime +import java.util.concurrent.TimeUnit +import javax.cache.Cache +import javax.cache.CacheManager +import javax.cache.Caching +import javax.cache.configuration.CompleteConfiguration +import javax.cache.configuration.FactoryBuilder +import javax.cache.configuration.MutableConfiguration +import javax.cache.expiry.CreatedExpiryPolicy +import javax.cache.expiry.Duration + +@Configuration +class ValidationConfiguration { + + private val NONCE_TTL_MINUTES: Long = 5 + private val CACHE_NAME = "nonceCache" + private val CERTS_RESOURCE_PATH = "/certs/" + private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks" + private val TRUSTSTORE_PASSWORD = "changeit" + + @Bean + fun cacheManager(): CacheManager { + return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager + } + + @Bean + fun nonceCache(): Cache? { + val cacheManager: CacheManager = cacheManager() + var cache = + cacheManager.getCache(CACHE_NAME) + if (cache == null) { + cache = createNonceCache(cacheManager) + } + return cache + } + + @Bean + fun generator(): NonceGenerator? { + return NonceGeneratorBuilder() + .withNonceTtl(java.time.Duration.ofMinutes(NONCE_TTL_MINUTES)) + .withNonceCache(nonceCache()) + .build() + } + + private fun createNonceCache(cacheManager: CacheManager): Cache? { + val cacheConfig: CompleteConfiguration = MutableConfiguration() + .setTypes(String::class.java, ZonedDateTime::class.java) + .setExpiryPolicyFactory( + FactoryBuilder.factoryOf( + CreatedExpiryPolicy( + Duration( + TimeUnit.MINUTES, + NONCE_TTL_MINUTES + 1 + ) + ) + ) + ) + return cacheManager.createCache(CACHE_NAME, cacheConfig) + } + + @Bean + fun loadTrustedCACertificatesFromCerFiles() : Array { + val caCertificates = ArrayList() + + try { + val certFactory = CertificateFactory.getInstance("X.509") + val resolver = PathMatchingResourcePatternResolver() + val resources = resolver.getResources("$CERTS_RESOURCE_PATH/*.cer") + + resources.forEach { resource -> + val caCertificate = certFactory.generateCertificate(resource.inputStream) as X509Certificate + caCertificates.add(caCertificate) + } + } catch (e : Exception) { + when (e){ + is CertificateException, is IOException -> { + throw RuntimeException("Error initializing trusted CA certificates. $e") + } + } + } + return caCertificates.toTypedArray() + } + + @Bean + fun loadTrustedCACertificatesFromTrustStore() : Array { + val caCertificates = ArrayList() + + ValidationConfiguration::class.java.getResourceAsStream("$CERTS_RESOURCE_PATH/$TRUSTED_CERTIFICATES_JKS").use { inputStream -> + try { + if (inputStream == null) { + // No truststore files found. + return arrayOf() + } + + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(inputStream, TRUSTSTORE_PASSWORD.toCharArray()) + val aliases = keyStore.aliases() + + while (aliases.hasMoreElements()) { + val alias = aliases.nextElement() + val certificate = keyStore.getCertificate(alias) as X509Certificate + caCertificates.add(certificate) + } + + + } catch (e : Exception) { + when (e) { + is IOException, is CertificateException, is KeyStoreException, is NoSuchAlgorithmException -> { + throw RuntimeException("Error initializing trusted CA certificates from trust store. $e") + } + } + } + } + + return caCertificates.toTypedArray() + } + + @Bean + fun validator() : AuthTokenValidator { + try { + return AuthTokenValidatorBuilder() + .withSiteOrigin(URI.create(ApplicationConfiguration.WEBSITE_ORIGIN_URL)) + .withNonceCache(nonceCache()) + .withTrustedCertificateAuthorities(*loadTrustedCACertificatesFromCerFiles()) + .withTrustedCertificateAuthorities(*loadTrustedCACertificatesFromTrustStore()) + .build() + } catch (e : JceException) { + throw RuntimeException("Error building the Web eID auth token validator.", e) + } + } + + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt new file mode 100644 index 0000000..9703034 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt @@ -0,0 +1,3 @@ +package com.tarkvaratehnika.demobackend.dto + +data class ChallengeDto(val nonce : String) \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt new file mode 100644 index 0000000..6b50be8 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt @@ -0,0 +1,6 @@ +package com.tarkvaratehnika.demobackend.security + +import com.fasterxml.jackson.annotation.JsonProperty + +class AuthTokenDTO (val token : String, val challenge : String) { +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt new file mode 100644 index 0000000..6bdeab7 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, 2021 The Web eID Project + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.tarkvaratehnika.demobackend.security + +import com.tarkvaratehnika.demobackend.config.ValidationConfiguration +import org.springframework.security.authentication.AuthenticationServiceException +import org.springframework.security.core.Authentication +import org.springframework.security.core.AuthenticationException +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.stereotype.Component +import org.webeid.security.exceptions.TokenValidationException +import org.webeid.security.validator.AuthTokenValidator +import java.security.cert.CertificateEncodingException +import java.security.cert.X509Certificate + + +@Component +class AuthTokenDTOAuthenticationProvider { + + companion object { + const val ROLE_USER : String = "ROLE_USER" + } + private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER) + + + val tokenValidator: AuthTokenValidator = ValidationConfiguration().validator() + + @Throws(AuthenticationException::class) + fun authenticate(auth : Authentication) : Authentication { + val authentication = auth as PreAuthenticatedAuthenticationToken + val token = (authentication.credentials as AuthTokenDTO).token + val challenge = (authentication.credentials as AuthTokenDTO).challenge + + val authorities = arrayListOf() + authorities.add(USER_ROLE) + + try { + val userCertificate: X509Certificate = tokenValidator.validate(token) + return WebEidAuthentication.fromCertificate(userCertificate, authorities, challenge) + } catch (e : TokenValidationException) { + // Validation failed. + throw AuthenticationServiceException("Token validation failed. " + e.message) + } catch (e : CertificateEncodingException) { + // Failed to extract subject fields from the certificate. + throw AuthenticationServiceException("Incorrect certificate subject fields: " + e.message) + } + } + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt new file mode 100644 index 0000000..c87ed42 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, 2021 The Web eID Project + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.tarkvaratehnika.demobackend.security + +import org.webeid.security.certificate.CertificateData + +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import java.security.cert.X509Certificate +import java.util.* +import java.util.concurrent.ThreadLocalRandom +import kotlin.collections.ArrayList +import kotlin.math.log + +class WebEidAuthentication( + private val principalName: String, + private val idCode: String, + private val authorities: ArrayList +) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication { + + // Companion object is for static functions. + companion object { + + private val loggedInUsers = HashMap() + + fun fromCertificate( + userCertificate: X509Certificate, + authorities: ArrayList, + challenge: String + ): Authentication { + val principalName = getPrincipalNameFromCertificate(userCertificate) + val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate)) + val authentication = WebEidAuthentication(principalName, idCode, authorities) + loggedInUsers[challenge] = authentication + return authentication + } + + /** + * Function for getting a Spring authentication object by supplying a challenge. + * TODO: Figure out a more secure solution in the future. + */ + fun fromChallenge(challenge: String): Authentication? { +// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing. +// return null +// } + val auth = loggedInUsers[challenge] + if (auth != null) { + // If challenge is valid, delete the authentication object from the map (so this can only be fetched once). + loggedInUsers.remove(challenge) + } else { + return null + } + return auth + } + +// // TODO: DELETE +// +// const val ROLE_USER: String = "ROLE_USER" +// private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER) +// +// fun addAuth(challenge: String) { +// val authorities = arrayListOf() +// authorities.add(USER_ROLE) +// val auth = WebEidAuthentication("Somename", "11111111111", authorities) +// loggedInUsers[challenge] = auth +// } +// +// +// // TODO: DELETE UNTIL + + private fun getPrincipalNameFromCertificate(userCertificate: X509Certificate): String { + return Objects.requireNonNull(CertificateData.getSubjectGivenName(userCertificate)) + " " + + Objects.requireNonNull(CertificateData.getSubjectSurname(userCertificate)) + } + } + + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt new file mode 100644 index 0000000..e4c6535 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt @@ -0,0 +1,20 @@ +package com.tarkvaratehnika.demobackend.web + +import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class LoginController { + + @GetMapping + fun login(model : Model) : String { + model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT) + model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL) + model.addAttribute("originUrl", ApplicationConfiguration.WEBSITE_ORIGIN_URL) + model.addAttribute("loggedInUrl", "/signature") + model.addAttribute("authenticationRequestUrl", ApplicationConfiguration.AUTHENTICATION_ENDPOINT_URL) + return "index" + } +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt new file mode 100644 index 0000000..bcc12ad --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt @@ -0,0 +1,20 @@ +package com.tarkvaratehnika.demobackend.web + +import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class SignatureController { + + + @PreAuthorize("hasAuthority('$ROLE_USER')") + @GetMapping("signature") + fun signature(model : Model) : String { +// model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT) +// model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL) + return "signature" + } +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt new file mode 100644 index 0000000..dc93dd0 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt @@ -0,0 +1,38 @@ +package com.tarkvaratehnika.demobackend.web.rest + +import com.tarkvaratehnika.demobackend.security.AuthTokenDTO +import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider +import com.tarkvaratehnika.demobackend.security.WebEidAuthentication +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ResponseStatusException + +@RestController +@RequestMapping("auth") +class AuthenticationController { + + private val LOG = LoggerFactory.getLogger(AuthenticationController::class.java) + + + @PostMapping("authentication", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun authenticate(@RequestBody authToken : AuthTokenDTO): Authentication { + // Create Spring Security Authentication object with supplied token as credentials. + val auth = PreAuthenticatedAuthenticationToken(null, authToken) + + // Return authentication object if success. + return AuthTokenDTOAuthenticationProvider().authenticate(auth) + } + + @GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE]) + fun getAuthenticated(@RequestParam challenge: String) : Authentication? { + val auth = WebEidAuthentication.fromChallenge(challenge) + if (auth == null) { + throw ResponseStatusException(HttpStatus.FORBIDDEN, "Not allowed.") + } + return auth + } +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt new file mode 100644 index 0000000..ae29d63 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, 2021 The Web eID Project + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.tarkvaratehnika.demobackend.web.rest + +import com.tarkvaratehnika.demobackend.dto.ChallengeDto +import com.tarkvaratehnika.demobackend.security.WebEidAuthentication +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.webeid.security.nonce.NonceGenerator + +@RestController +@RequestMapping("auth") +class ChallengeController (val nonceGenerator: NonceGenerator) { + + + @GetMapping("challenge") + fun challenge(): ChallengeDto { + val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce()) +// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing. + return challengeDto + } + +} + diff --git a/demoBackend/src/main/resources/application.properties b/demoBackend/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/demoBackend/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer b/demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer new file mode 100644 index 0000000000000000000000000000000000000000..b16695560fd7f7498f20dedd8ac67098f6eeee57 GIT binary patch literal 1652 zcmXqLVk7~D2aMlsHU>`aH^Y`xS}0|jwj zLsLT|LvuqTV+#}GC<%Te17ib_fRVAOrKx3# zz|hzbD#6Ud=NcU1>gl2z?5$vAU}$R4#H55AAdIXG%uP)E3_x)%rY0svhD(jVu3Y$- zdv)*Rlc$-Z+A{VrzWXzG;oX))^Dp^`Io@Z~RhZCS=H;?cUo-Y&lG_^QuoG;OyZ1TY z&s`VxQ>X7!_pTEYrSzX%f6IEM*H+bX4M&76M}F!Q1#6Zl*>x_bA4KU)e5SWd!23qP zUT&sj9i88z`6i*7+Fow_>y%EOnrrkR^xyWPr3-vOBNkXD#Lh-*2P4$x$Xx&sBFWoh{-1IZgvIBe5mKc1o zY3y7Ttz-CNoqCgT7GrdSOPh(Q>?3uxOCrJ_G$&MgZk60RkF{HF#Sb6H`f7&&@0dwf zPp34UYA}8ywRHub`R|7^otx`Uh?~ykHcRW;Q{?ZX?RMyhv0RXg$oZvl4p~)SI{Tb< z8$4b$`Ml@aQm!+eJDy&$6xg_)+!b zO7ANoGYw=x3iw#WSVT6j?$|T)YSNNcEsnEWWQ(kui`)YZ_(0P9jEw(TSb&+6&7d5_ zS70eH$TQ$zV`E|HuVQ2ZW=3`vhVC~GK&Am3r#2fS3*$L?Mn(f=14T9tAeWVuorzIQ zG^3=Xpx8=Zzr4I$51gg+odbdmxDh&-kU7lA92Ns1kj26vhj19M0V!ZgH*f@r$+1`& zSS&O%U;$L4RjSriQ9U@m3mgtM67EEa8CVDv>$|$fqm)KK9;h^m2bD(g&W?I1 zsYQCpMI{EdAZN<6m>HND7%ebZpx35@VPH{eL4I*&Nq$kKesWPxv3_c5a&l2}B2aq{ za(MwPK$)8u85v5#c#_QReYI~LUOw&9aV`Hi;jMq~KKb|XU2x?R9jl3!XIVo0ZYv(S z&p2Op@V?mgOcP?e^&Swgg`8Lk9Lwc8aMT*f_WD$n`_=Ts!uy z^C+wf{uaFH+$!mv8#xO1CVihh;a)`luN!XPYX3c|U8ml7TE^l~-|g$S%5Hpp!sL4X zx$eIe>(5o)ubH-nS`!$APM4d@dyQbD|osn1n1|JmSpDV6)QLf8;To|jqaF|x60H?lA&GdH#|urMCCkj#JU zbxl}#{4EZD8=UiQ7|m7nS=nAVC+d`}>%&(UQ<|iY@8C_jVX|U&gv&Fph3r#v zmY6TgXKhvyo)F2{r~Fs;A0VE5xKdh;C6i7z9~NzJlW^I=z_`GSz1B{d?0Cl zM#ldvEWns%GY|stg+Y7{12!PV#K>UK#CR4Ys=(63cm&8fV8Fq~#=_8F#mEGVc6Jtq z?l%rVrV$&rHX9=gWAj8sMn*;hWdlVv4xj)lD?1aTm}o{xNkOrdzJ7Umxn6O$UTUho zb3m{GFR~scByK&(2ozaXR4yATmmQVMjLKy}<#M2MIZ?S>s9bJTE)Ob~7nRG0%H>Dp z3ZQZYQMp2>TwzqM2oe_|dZPvVVbG6UH}SY?B4WDlk$LGZUeanwqMco?n)n2+l$JaHULb%21`rP=&~u zTrVZHNH4jl#6SY3fsvVo$AAlx&idSdNx=Z*VtE!#19bz{1ryv>4iIF*z|RuVU`CR9u&?WpdKyHoH@>(wk*DibsxH vHN+Il?|g7yGCo*=t>@%SyV|wW>~DXbCmgq(g@^f~dx!()8JmCmqDx}|!-qnr literal 0 HcmV?d00001 diff --git a/demoBackend/src/main/resources/certs/trusted_certificates.jks b/demoBackend/src/main/resources/certs/trusted_certificates.jks new file mode 100644 index 0000000000000000000000000000000000000000..0276f80a9d40486ac22b88b73f38cd5bf513e9ed GIT binary patch literal 1345 zcmezO_TO6u1_mY|W(3nbsj0f@`DMw8Mh1o!K*17~*)={4tPy&q29^vAEPo7|SbhL; z=1>+kVJ25s zLums^5QmFLDA-%U(?uaTKc}=LGe56b!7|k#)F|x60 zH?lA&GdH#|urM5#{(G~bFv>nZZL`3aU7TNc*EpDIJv_xd_wZYdP_s{;AF?WQUb)_T z+y2q}+FmaSLH0S*o`#+|nIJHwbe?lr#jDed(+urw*q&5$TvKy7nIE7T9w)@JzTI@1 z&&6ZCvByiNo7IfrJ$1IZ->Nt=HwVmSU);pB$DoO6 zhk+0<*kpwn8UM3z7_b2;CPqdBK9CqcNQ?y-e{2ROAigSyuVtXY#-Yu|$jZvj%n4^P z!C5SD7NdbINEaWA7>h`qX4rF+NfmzO5A*emwPq9^{A)7bKprHm%pzeR)_`3>6B7eS zi2_R#<9CB5#?J;EY-}tH{Z))iz~sWt!qEN30mw9B;|98ig|T^}A|uf0$_9!sConOJ ziDs0P6ck(O>z9|8>lJ6~rKajT2Lv1NBI{v7;?{$VK#^rd<+7o2*-^R7s9Y9QE(a=? z6P3$_%H>Ao@}P2gQMr7mTz*up04i4yl`DkG6-MQXAaQ|#gBDiE{sqb+`xnSX4mu#0 z&A^b23l>#eM8=c^EHW9HS$GV%AW5Ll4H!#EB?xn44}(Et2a^-yfM!fRmaZ38ZS>}Fc%JrmMb(`b3{qB=J6`cV{c0W w0#2|9f1bD@Zq7c%U;NjCce=elexl`<`}>(IGj_5!BoukZADy|;Cn>`Q0M189!T * { + margin: 1rem; +} \ No newline at end of file diff --git a/demoBackend/src/main/resources/static/js/index.js b/demoBackend/src/main/resources/static/js/index.js new file mode 100644 index 0000000..083655f --- /dev/null +++ b/demoBackend/src/main/resources/static/js/index.js @@ -0,0 +1,14 @@ +window.onload = () => { + // Add event listener for login button. + let loginButton = document.getElementById("loginButton"); + + if (loginButton != null) { + loginButton.addEventListener("click", () => { + let action = loginButton.getAttribute("data-action"); + loginButton.setAttribute("disabled", "true"); + loginButton.textContent = "Logging in"; + launchAuthApp(action); + }) + } +} + diff --git a/demoBackend/src/main/resources/static/js/main.js b/demoBackend/src/main/resources/static/js/main.js new file mode 100644 index 0000000..4e2f7f0 --- /dev/null +++ b/demoBackend/src/main/resources/static/js/main.js @@ -0,0 +1,67 @@ +const POLLING_INTERVAL = 1000; +const POLLING_RETRIES = 120; + +function launchAuthApp(action) { + if (!isAndroid()) { + alert("Functionality only available for Android devices.") + return null + } + + // Fetch challenge. + httpGetAsync(originUrl + challengeUrl, (body) => { + let data = JSON.parse(body); + let challenge = data.nonce; + let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling. + console.log(intent); + window.location.href = intent; + pollForAuth(POLLING_INTERVAL, challenge); + }) +} + +function pollForAuth(timeout, challenge) { + console.log("Polling for auth"); + let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + challenge; + + let counter = 0; + let timer = setInterval(() => { + // Fetch authentication object. + httpGetAsync(requestUrl, (body) => { + console.log(body); + // If this is a successful request, stop the polling. + clearInterval(timer); + window.location.href = originUrl + loggedInUrl; + }); + counter++; + if (counter > POLLING_RETRIES) { + clearInterval(timer); // Stop polling after some time. + let loginErrorAlert = document.getElementById("loginErrorAlert"); + loginErrorAlert.classList.remove("d-none") + } + }, timeout) + +} + +function createParametrizedIntentUrl(challenge, action) { + if (action == null) { + console.error("There has to be an action for intent.") + } + return intentUrl + "?" + "action=" + action + (challenge != null ? "&challenge=" + challenge : ""); +} + +function isAndroid() { + // Check if using Android device. + const ua = navigator.userAgent.toLowerCase(); + return ua.indexOf("android") > -1; +} + +function httpGetAsync(theUrl, callback) { + console.log("Sending a request.") + const xmlHttp = new XMLHttpRequest(); + xmlHttp.onreadystatechange = function () { + if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { + callback(xmlHttp.responseText); + } + } + xmlHttp.open("GET", theUrl, true); // true for asynchronous + xmlHttp.send(null); +} \ No newline at end of file diff --git a/demoBackend/src/main/resources/templates/index.html b/demoBackend/src/main/resources/templates/index.html new file mode 100644 index 0000000..e6ae490 --- /dev/null +++ b/demoBackend/src/main/resources/templates/index.html @@ -0,0 +1,38 @@ + + + + Login + + + + + + + + + + + +
+

Welcome to Estonian ID card mobile authentication demo website. When using a mobile phone, you can log in to the + website using your ID card by using the button below.

+
Make sure you've installed the authentication app from: GitHub
+ + +
+ + \ 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..0c8ce1c --- /dev/null +++ b/demoBackend/src/main/resources/templates/signature.html @@ -0,0 +1,42 @@ + + + + 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() { + } + +} From e4a9a4da1b16654d1c0c81d85997df6453f015ec Mon Sep 17 00:00:00 2001 From: TanelOrumaa Date: Mon, 8 Nov 2021 17:49:42 +0200 Subject: [PATCH 10/18] Added readme to backend demo project --- demoBackend/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 demoBackend/README.md diff --git a/demoBackend/README.md b/demoBackend/README.md new file mode 100644 index 0000000..73cfe53 --- /dev/null +++ b/demoBackend/README.md @@ -0,0 +1,39 @@ +# Demo backend + website for mobile authentication project. + +## How to run. + + +### 1. Clone GIT repository +### 2. Setup HTTPS +Since Web eID only works over HTTPS connection, you'll need to serve the backend and website with an HTTPS certificate. A suitable tool for that is ngrok (https://ngrok.com/). +To use ngrok, download it and then run the command (may need administrator rights) +```ngrok http 8080``` +and you should see something like this: +``` +ngrok by @inconshreveable (Ctrl+C to quit) + +Session Status online +Account TanelOrumaa (Plan: Free) +Version 2.3.40 +Region United States (us) +Web Interface http://127.0.0.1:4040 +Forwarding http://somethinghere.ngrok.io -> http://localhost:8080 +Forwarding https://somethinghere.ngrok.io -> http://localhost:8080 + +Connections ttl opn rt1 rt5 p50 p90 + 1508 0 0.00 0.00 2.31 75.59 + +HTTP Requests +------------- +``` + +Copy the second forwarding link (the one with https) and put it in ```com.tarkvaratehnika.demobackend.config.ApplicationConfiguration.kt``` as ```val WEBSITE_ORIGIN_URL = "https://yourlinkhere.com"``` + +### 3. Run the project +Use your favourite IDE or just run it via commandline with ```./mvnw spring-boot:run``` + +On your Android device browser navigate to the url you copied earlier and you should see the website landing page. If you have the mobile authentication app installed, you should be able to log into the website with your Estonian ID-card. + + +## Credits... +...go out to creators of https://github.com/web-eid/web-eid-spring-boot-example. That example project was used in some parts as an example (files where inspiration was taken are correctly annotated with the appropriate license text). \ No newline at end of file From 82b153886728cc2bb59865f24d69d604929350af Mon Sep 17 00:00:00 2001 From: TanelOrumaa Date: Mon, 8 Nov 2021 22:41:09 +0200 Subject: [PATCH 11/18] MOB-42 Fixed JWT generation issue. --- .../mobileauthapp/AuthFragment.kt | 91 ++++++++----------- .../mobileauthapp/NFC/Comms.java | 1 + .../mobileauthapp/auth/Authenticator.kt | 42 ++++----- .../src/main/resources/static/js/main.js | 6 +- 4 files changed, 63 insertions(+), 77 deletions(-) diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt index 91f0811..6cf616e 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt @@ -1,6 +1,7 @@ package com.tarkvaraprojekt.mobileauthapp -import android.content.Intent +import android.app.Activity +import android.content.Context import android.nfc.NfcAdapter import android.nfc.tech.IsoDep import android.os.Bundle @@ -9,16 +10,15 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs import com.tarkvaraprojekt.mobileauthapp.NFC.Comms import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import java.lang.Exception +import kotlin.concurrent.thread /** * Fragment that asks the user to detect the ID card with mobile NFC chip. @@ -31,8 +31,6 @@ class AuthFragment : Fragment() { private var binding: FragmentAuthBinding? = null - private val args: CanFragmentArgs by navArgs() - private lateinit var timer: CountDownTimer private var timeRemaining: Int = 90 @@ -71,66 +69,51 @@ class AuthFragment : Fragment() { } private fun getInfoFromIdCard(adapter: NfcAdapter) { - if (args.reading) { - 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) - val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8)) - viewModel.setUserFirstName(response[1]) - viewModel.setUserLastName(response[0]) - viewModel.setUserIdentificationNumber(response[2]) - 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) + 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) + val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8)) + viewModel.setUserFirstName(response[1]) + viewModel.setUserLastName(response[0]) + viewModel.setUserIdentificationNumber(response[2]) + 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) } - }, 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() { timer.cancel() - if (args.auth) { - val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile) - findNavController().navigate(action) - } else { - findNavController().navigate(R.id.action_authFragment_to_userFragment) - } + findNavController().navigate(R.id.action_authFragment_to_userFragment) } private fun goToTheStart() { viewModel.clearUserInfo() timer.cancel() - if (args.reading) { - findNavController().navigate(R.id.action_authFragment_to_homeFragment) - } else { - val resultIntent = Intent() - requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) - requireActivity().finish() - } + findNavController().navigate(R.id.action_authFragment_to_homeFragment) } override fun onDestroy() { 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 76e1c3a..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; 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 0d7c862..383ad68 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,16 +1,12 @@ package com.tarkvaraprojekt.mobileauthapp.auth -import android.os.Message import android.util.Log import com.tarkvaraprojekt.mobileauthapp.NFC.Comms import io.jsonwebtoken.SignatureAlgorithm -import org.bouncycastle.util.encoders.Base64 import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.time.LocalDateTime import java.time.ZoneOffset -import javax.crypto.Mac -import kotlin.experimental.and class Authenticator(val comms : Comms) { @@ -19,43 +15,45 @@ class Authenticator(val comms : Comms) { 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 { + 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.getCertificate(true); + val authenticationCertificate: ByteArray = comms.getCertificate(true); // Encode the certificate in base64. - val base64cert = String(Base64.encode(authenticationCertificate)) + val base64cert = java.util.Base64.getEncoder().encodeToString(authenticationCertificate) // 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() + val exp = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(5 * 60L).atZone(ZoneOffset.UTC) + .toEpochSecond() - // Get subject value. - val sub = authenticationCertificate[0] // TODO: + // 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 header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}""" + val claims = + """{"iat":"$epoch","exp":"$exp","aud":"$originUrl","iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}""" - var jwt = String(Base64.encode(header.toByteArray(Charsets.UTF_8))) + "." + String(Base64.encode(claims.toByteArray(Charsets.UTF_8))) - jwt = jwt.replace("=", "") - - Log.v("JWT", jwt) + 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()) + val encoded = + MessageDigest.getInstance("SHA-384").digest(jwt.toByteArray(StandardCharsets.UTF_8)) val signed = comms.authenticate(pin1, encoded) - val jws = jwt + "." + String(Base64.encode(signed)) - - Log.v("Token", jws) - // Return the signed authentication token. - return jws + 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/demoBackend/src/main/resources/static/js/main.js b/demoBackend/src/main/resources/static/js/main.js index 4e2f7f0..f38d255 100644 --- a/demoBackend/src/main/resources/static/js/main.js +++ b/demoBackend/src/main/resources/static/js/main.js @@ -45,7 +45,11 @@ function createParametrizedIntentUrl(challenge, action) { if (action == null) { console.error("There has to be an action for intent.") } - return intentUrl + "?" + "action=" + action + (challenge != null ? "&challenge=" + challenge : ""); + else if (challenge == null) { + console.error("Challenge missing, can't authenticate without it.") + } else { + return intentUrl + "?" + "action=" + action + "&challenge=" + challenge + "&authUrl=" + originUrl + authenticationRequestUrl; + } } function isAndroid() { From 9b0cb1a22da7460184096ec6df39e1a974967df9 Mon Sep 17 00:00:00 2001 From: TanelOrumaa Date: Mon, 8 Nov 2021 23:52:38 +0200 Subject: [PATCH 12/18] MOB-42 Changed POST request logic --- MobileAuthApp/app/build.gradle | 2 + .../mobileauthapp/ResultFragment.kt | 51 +++++++++++-------- .../mobileauthapp/network/TokenApiService.kt | 4 +- .../web/rest/AuthenticationController.kt | 4 +- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/MobileAuthApp/app/build.gradle b/MobileAuthApp/app/build.gradle index 8cff52f..bc4a034 100644 --- a/MobileAuthApp/app/build.gradle +++ b/MobileAuthApp/app/build.gradle @@ -67,6 +67,8 @@ dependencies { '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/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt index d2eb5fc..6eddd05 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt @@ -10,6 +10,8 @@ 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 @@ -58,27 +60,36 @@ class ResultFragment : Fragment() { * 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) - Log.v("Response", response.message()) - if (response.isSuccessful) { - Log.v("GREAAAT", "SUCCESSSS") - //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).getConscryptMiddleware().enable(false) + + Ion.with(activity) + .load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication") + .setJsonObjectBody(json) + .asJsonObject() + .setCallback { e, result -> + // do stuff with the result or error + Log.i("Log thingy", result.toString()) } - } - } +// CoroutineScope(Dispatchers.Default).launch { +// val response = TokenApi.retrofitService.postToken(jsonBody) +// Log.v("Response", response.message()) +// 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) +// } +// } +// } } /** 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 7e7d70f..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 @@ -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/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt index dc93dd0..68a3f5b 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt @@ -19,7 +19,9 @@ class AuthenticationController { @PostMapping("authentication", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) - fun authenticate(@RequestBody authToken : AuthTokenDTO): Authentication { + fun authenticate(@RequestBody body : String): Authentication { + val parts = body.split("\"") + val authToken = AuthTokenDTO(parts[3], parts[7]) // Create Spring Security Authentication object with supplied token as credentials. val auth = PreAuthenticatedAuthenticationToken(null, authToken) From 636beeb7f3df0578755d889d0e11fb3c0b38411f Mon Sep 17 00:00:00 2001 From: TanelOrumaa Date: Thu, 11 Nov 2021 21:47:27 +0200 Subject: [PATCH 13/18] MOB-42 Fixed token authentication issues (wrong library version, cache getting recreated every request, origin in wrong form) --- .../mobileauthapp/AuthFragment.kt | 2 +- .../mobileauthapp/HomeFragment.kt | 9 ++++++++- .../mobileauthapp/ResultFragment.kt | 2 +- .../mobileauthapp/auth/Authenticator.kt | 6 +++--- .../mobileauthapp/model/ParametersViewModel.kt | 7 +++++++ demoBackend/pom.xml | 5 +++++ .../config/ApplicationConfiguration.kt | 2 +- .../config/ValidationConfiguration.kt | 17 ++++++++++++++++- .../AuthTokenDTOAuthenticationProvider.kt | 12 +++++++----- .../demobackend/web/SignatureController.kt | 2 +- .../web/rest/AuthenticationController.kt | 2 +- .../demobackend/web/rest/ChallengeController.kt | 3 +++ .../src/main/resources/static/css/main.css | 4 ++++ .../src/main/resources/static/js/main.js | 8 ++++---- .../src/main/resources/templates/index.html | 4 ++-- 15 files changed, 64 insertions(+), 21 deletions(-) diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt index ed6de75..3adfdf4 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt @@ -90,7 +90,7 @@ class AuthFragment : Fragment() { if (args.auth) { val jws = Authenticator(comms).authenticate( intentParameters.challenge, - intentParameters.authUrl, + intentParameters.origin, viewModel.userPin ) intentParameters.setToken(jws) 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 f0b60e6..dff550a 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,16 @@ 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")!!) + 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/ResultFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt index 6eddd05..98e9cac 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt @@ -67,7 +67,7 @@ class ResultFragment : Fragment() { Ion.getDefault(activity).getConscryptMiddleware().enable(false) Ion.with(activity) - .load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication") + .load(paramsModel.origin + paramsModel.authUrl) .setJsonObjectBody(json) .asJsonObject() .setCallback { e, result -> 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 383ad68..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 @@ -8,7 +8,7 @@ import java.security.MessageDigest import java.time.LocalDateTime import java.time.ZoneOffset -class Authenticator(val comms : Comms) { +class Authenticator(val comms: Comms) { val type = "JWT" val algorithm = "ES384" @@ -36,7 +36,7 @@ class Authenticator(val comms : Comms) { // 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":""}}""" + """{"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) @@ -51,7 +51,7 @@ class Authenticator(val comms : Comms) { return jwt + "." + base64Encode(signed) } - fun base64Encode(bytes: ByteArray) : String? { + fun base64Encode(bytes: ByteArray): String? { val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes) return encoded.replace("=", "") } 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/demoBackend/pom.xml b/demoBackend/pom.xml index bd4ba2a..8b1f7a3 100644 --- a/demoBackend/pom.xml +++ b/demoBackend/pom.xml @@ -42,6 +42,11 @@ spring-boot-starter-test test + + com.squareup.okhttp3 + okhttp + 4.9.0 + org.webeid.security authtoken-validation diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt index d025eed..abee1f8 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt @@ -10,7 +10,7 @@ class ApplicationConfiguration { // Endpoint for authentication val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication" // URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here. - val WEBSITE_ORIGIN_URL = "https://6bb0-85-253-195-252.ngrok.io" + val WEBSITE_ORIGIN_URL = "https://2c2c-85-253-195-252.ngrok.io" } } \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt index 237fcdd..3bdefc4 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt @@ -1,6 +1,8 @@ package com.tarkvaratehnika.demobackend.config import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.support.PathMatchingResourcePatternResolver @@ -28,14 +30,25 @@ import javax.cache.configuration.MutableConfiguration import javax.cache.expiry.CreatedExpiryPolicy import javax.cache.expiry.Duration +import javax.cache.configuration.FactoryBuilder.factoryOf + @Configuration class ValidationConfiguration { + private val LOG: Logger = LoggerFactory.getLogger(ValidationConfiguration::class.java) + private val NONCE_TTL_MINUTES: Long = 5 private val CACHE_NAME = "nonceCache" private val CERTS_RESOURCE_PATH = "/certs/" private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks" private val TRUSTSTORE_PASSWORD = "changeit" + companion object { + const val ROLE_USER : String = "ROLE_USER" + } + + init { + LOG.warn("Creating new ValidationConfiguration.") + } @Bean fun cacheManager(): CacheManager { @@ -47,7 +60,9 @@ class ValidationConfiguration { val cacheManager: CacheManager = cacheManager() var cache = cacheManager.getCache(CACHE_NAME) + if (cache == null) { + LOG.warn("Creating new cache.") cache = createNonceCache(cacheManager) } return cache @@ -65,7 +80,7 @@ class ValidationConfiguration { val cacheConfig: CompleteConfiguration = MutableConfiguration() .setTypes(String::class.java, ZonedDateTime::class.java) .setExpiryPolicyFactory( - FactoryBuilder.factoryOf( + factoryOf( CreatedExpiryPolicy( Duration( TimeUnit.MINUTES, diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt index 6bdeab7..122eb1b 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt @@ -23,6 +23,9 @@ package com.tarkvaratehnika.demobackend.security import com.tarkvaratehnika.demobackend.config.ValidationConfiguration +import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER +import com.tarkvaratehnika.demobackend.web.rest.AuthenticationController +import org.slf4j.LoggerFactory import org.springframework.security.authentication.AuthenticationServiceException import org.springframework.security.core.Authentication import org.springframework.security.core.AuthenticationException @@ -37,11 +40,11 @@ import java.security.cert.X509Certificate @Component -class AuthTokenDTOAuthenticationProvider { +object AuthTokenDTOAuthenticationProvider { + + private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java) + - companion object { - const val ROLE_USER : String = "ROLE_USER" - } private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER) @@ -52,7 +55,6 @@ class AuthTokenDTOAuthenticationProvider { val authentication = auth as PreAuthenticatedAuthenticationToken val token = (authentication.credentials as AuthTokenDTO).token val challenge = (authentication.credentials as AuthTokenDTO).challenge - val authorities = arrayListOf() authorities.add(USER_ROLE) diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt index bcc12ad..d526fec 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt @@ -1,6 +1,6 @@ package com.tarkvaratehnika.demobackend.web -import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER +import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER import org.springframework.security.access.prepost.PreAuthorize import org.springframework.stereotype.Controller import org.springframework.ui.Model diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt index 68a3f5b..6414441 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt @@ -26,7 +26,7 @@ class AuthenticationController { val auth = PreAuthenticatedAuthenticationToken(null, authToken) // Return authentication object if success. - return AuthTokenDTOAuthenticationProvider().authenticate(auth) + return AuthTokenDTOAuthenticationProvider.authenticate(auth) } @GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE]) diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt index ae29d63..7333c9d 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt @@ -24,6 +24,7 @@ package com.tarkvaratehnika.demobackend.web.rest import com.tarkvaratehnika.demobackend.dto.ChallengeDto import com.tarkvaratehnika.demobackend.security.WebEidAuthentication +import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -33,10 +34,12 @@ import org.webeid.security.nonce.NonceGenerator @RequestMapping("auth") class ChallengeController (val nonceGenerator: NonceGenerator) { + private val LOG = LoggerFactory.getLogger(ChallengeController::class.java) @GetMapping("challenge") fun challenge(): ChallengeDto { val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce()) + LOG.warn(challengeDto.nonce) // WebEidAuthentication.addAuth(challengeDto.nonce) // For testing. return challengeDto } diff --git a/demoBackend/src/main/resources/static/css/main.css b/demoBackend/src/main/resources/static/css/main.css index b28c55e..31f3ebe 100644 --- a/demoBackend/src/main/resources/static/css/main.css +++ b/demoBackend/src/main/resources/static/css/main.css @@ -1,3 +1,7 @@ +html { + font-size: 4vw; +} + .cont { display: grid; width: 80%; diff --git a/demoBackend/src/main/resources/static/js/main.js b/demoBackend/src/main/resources/static/js/main.js index f38d255..46b607a 100644 --- a/demoBackend/src/main/resources/static/js/main.js +++ b/demoBackend/src/main/resources/static/js/main.js @@ -11,7 +11,7 @@ function launchAuthApp(action) { httpGetAsync(originUrl + challengeUrl, (body) => { let data = JSON.parse(body); let challenge = data.nonce; - let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling. + let intent = createParametrizedIntentUrl(challenge, action, originUrl); // TODO: Error handling. console.log(intent); window.location.href = intent; pollForAuth(POLLING_INTERVAL, challenge); @@ -20,8 +20,8 @@ function launchAuthApp(action) { function pollForAuth(timeout, challenge) { console.log("Polling for auth"); - let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + challenge; - + let encodedChallenge = encodeURIComponent(challenge); + let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + encodedChallenge; let counter = 0; let timer = setInterval(() => { // Fetch authentication object. @@ -48,7 +48,7 @@ function createParametrizedIntentUrl(challenge, action) { else if (challenge == null) { console.error("Challenge missing, can't authenticate without it.") } else { - return intentUrl + "?" + "action=" + action + "&challenge=" + challenge + "&authUrl=" + originUrl + authenticationRequestUrl; + return intentUrl + "?" + "action=" + action + "&challenge=" + encodeURIComponent(challenge) + "&authUrl=" + authenticationRequestUrl + "&originUrl=" + originUrl; } } diff --git a/demoBackend/src/main/resources/templates/index.html b/demoBackend/src/main/resources/templates/index.html index e6ae490..a436fea 100644 --- a/demoBackend/src/main/resources/templates/index.html +++ b/demoBackend/src/main/resources/templates/index.html @@ -21,7 +21,7 @@
@@ -29,7 +29,7 @@ website using your ID card by using the button below.
Make sure you've installed the authentication app from: GitHub
- + From 168c9be01024e7e3984eafe3b87d5c7ccde11905 Mon Sep 17 00:00:00 2001 From: Henrik Lepson Date: Sun, 14 Nov 2021 10:13:40 +0200 Subject: [PATCH 14/18] fixed app not closing bug, when started from website --- .../mobileauthapp/AuthFragment.kt | 4 +- .../mobileauthapp/CanFragment.kt | 12 ++-- .../mobileauthapp/HomeFragment.kt | 2 - .../mobileauthapp/PinFragment.kt | 12 ++-- .../mobileauthapp/ResultFragment.kt | 57 +++++++------------ .../app/src/main/res/values-en/strings.xml | 4 +- .../app/src/main/res/values-et/strings.xml | 4 +- .../app/src/main/res/values/strings.xml | 4 +- .../com/example/testmobileapp/MainActivity.kt | 39 ++++++++++--- .../app/src/main/res/layout/activity_main.xml | 36 +++++++++++- .../app/src/main/res/values-en/strings.xml | 1 + .../app/src/main/res/values-et/strings.xml | 1 + .../app/src/main/res/values/strings.xml | 1 + 13 files changed, 116 insertions(+), 61 deletions(-) diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt index 3adfdf4..bff62f4 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt @@ -69,6 +69,7 @@ class AuthFragment : Fragment() { goToTheStart() } }.start() + //binding!!.nextButton.visibility = View.INVISIBLE binding!!.nextButton.setOnClickListener { goToNextFragment() } binding!!.cancelButton.setOnClickListener { goToTheStart() } val adapter = NfcAdapter.getDefaultAdapter(activity) @@ -140,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 dff550a..e1888f4 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/HomeFragment.kt @@ -60,8 +60,6 @@ class HomeFragment : Fragment() { 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()) 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(" ", "+") 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 98e9cac..0f3d919 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt @@ -14,15 +14,6 @@ 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.BASE_URL -import com.tarkvaraprojekt.mobileauthapp.network.TokenApi -import com.tarkvaraprojekt.mobileauthapp.network.TokenApiService -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 @@ -48,12 +39,8 @@ class ResultFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding!!.resultBackButton.setOnClickListener { -// if (args.mobile) { -// createResponse() -// } - postToken() - } + binding!!.resultBackButton.visibility = View.GONE + postToken() } /** @@ -64,40 +51,40 @@ class ResultFragment : Fragment() { json.addProperty("token", paramsModel.token) json.addProperty("challenge", paramsModel.challenge) - Ion.getDefault(activity).getConscryptMiddleware().enable(false) - + 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 - Log.i("Log thingy", result.toString()) + 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("Log thingy success", result.toString()) + if (args.mobile) { + createResponse(true, result.toString(), paramsModel.token) + } else { + requireActivity().finishAndRemoveTask() + } + } } -// CoroutineScope(Dispatchers.Default).launch { -// val response = TokenApi.retrofitService.postToken(jsonBody) -// Log.v("Response", response.message()) -// 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) -// } -// } -// } } /** * 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/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/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt b/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt index 4184aad..73ad255 100644 --- a/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt +++ b/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt @@ -5,9 +5,11 @@ 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 /** @@ -18,9 +20,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 -> @@ -34,19 +38,21 @@ class MainActivity : AppCompatActivity() { } } - 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,8 +64,10 @@ 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 originUrl = "enter-base-url-here" + val originUrl = "https-origin-url-here" + val url = "$originUrl/auth/challenge" + Ion.getDefault(this).conscryptMiddleware.enable(false) Ion.with(applicationContext) .load(url) .asJsonObject() @@ -67,10 +75,27 @@ class MainActivity : AppCompatActivity() { try { // Get data from the result and call launchAuth method val challenge = result.asJsonObject["nonce"].toString() - launchAuth(challenge, baseUrl) + launchAuth(challenge, originUrl, "/auth/authentication") } catch (e: Exception) { Log.i("GETrequest", "was unsuccessful") } } } + + private fun showLogin() { + binding.loginOptions.visibility = View.VISIBLE + } + + private fun showResult(resultObject: String, token: String) { + binding.loginOptions.visibility = View.GONE + binding.resultLayout.visibility = View.VISIBLE + binding.resultObject.text = resultObject + binding.resultToken.text = token + binding.buttonForget.setOnClickListener { + binding.resultObject.text = "" + binding.resultToken.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..98727e4 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/TestMobileApp/app/src/main/res/values-en/strings.xml b/TestMobileApp/app/src/main/res/values-en/strings.xml index 86688c5..8188b11 100644 --- a/TestMobileApp/app/src/main/res/values-en/strings.xml +++ b/TestMobileApp/app/src/main/res/values-en/strings.xml @@ -6,4 +6,5 @@ NFC auth Successful response Response failed + Forget \ No newline at end of file diff --git a/TestMobileApp/app/src/main/res/values-et/strings.xml b/TestMobileApp/app/src/main/res/values-et/strings.xml index 0daf8b4..09391e1 100644 --- a/TestMobileApp/app/src/main/res/values-et/strings.xml +++ b/TestMobileApp/app/src/main/res/values-et/strings.xml @@ -6,4 +6,5 @@ NFC auth Vastus kätte saadud Vastust ei õnnestunud kätte saada + Unusta \ No newline at end of file diff --git a/TestMobileApp/app/src/main/res/values/strings.xml b/TestMobileApp/app/src/main/res/values/strings.xml index 2353f0b..6108518 100644 --- a/TestMobileApp/app/src/main/res/values/strings.xml +++ b/TestMobileApp/app/src/main/res/values/strings.xml @@ -5,4 +5,5 @@ NFC auth Successful response Response failed + Forget \ No newline at end of file From 5b70a8f99747058790083b9ee2e481f93169ec11 Mon Sep 17 00:00:00 2001 From: TanelOrumaa Date: Tue, 16 Nov 2021 21:30:58 +0200 Subject: [PATCH 15/18] MOB-42 Added log out button to backend, fixed issue with challenge for test app --- .../mobileauthapp/ResultFragment.kt | 2 +- .../com/example/testmobileapp/MainActivity.kt | 5 ++- .../config/ApplicationConfiguration.kt | 2 +- .../src/main/resources/static/css/main.css | 7 +++- .../main/resources/templates/signature.html | 37 ++++++++----------- 5 files changed, 26 insertions(+), 27 deletions(-) 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 0f3d919..e658c72 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/ResultFragment.kt @@ -67,7 +67,7 @@ class ResultFragment : Fragment() { requireActivity().finishAndRemoveTask() } } else { - Log.i("Log thingy success", result.toString()) + Log.i("POST request response", result.toString()) if (args.mobile) { createResponse(true, result.toString(), paramsModel.token) } else { 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 73ad255..d317f18 100644 --- a/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt +++ b/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt @@ -65,7 +65,7 @@ class MainActivity : AppCompatActivity() { private fun getData() { // Enter the server endpoint address to here //val originUrl = "enter-base-url-here" - val originUrl = "https-origin-url-here" + val originUrl = "https://5d0c-85-253-195-195.ngrok.io" val url = "$originUrl/auth/challenge" Ion.getDefault(this).conscryptMiddleware.enable(false) Ion.with(applicationContext) @@ -74,7 +74,8 @@ class MainActivity : AppCompatActivity() { .setCallback { _, result -> try { // Get data from the result and call launchAuth method - val challenge = result.asJsonObject["nonce"].toString() + val challenge = result.asJsonObject["nonce"].toString().replace("\"", "") + Log.v("Challenge", challenge) launchAuth(challenge, originUrl, "/auth/authentication") } catch (e: Exception) { Log.i("GETrequest", "was unsuccessful") diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt index abee1f8..ca49fdb 100644 --- a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt @@ -10,7 +10,7 @@ class ApplicationConfiguration { // Endpoint for authentication val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication" // URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here. - val WEBSITE_ORIGIN_URL = "https://2c2c-85-253-195-252.ngrok.io" + val WEBSITE_ORIGIN_URL = "https://5d0c-85-253-195-195.ngrok.io" } } \ No newline at end of file diff --git a/demoBackend/src/main/resources/static/css/main.css b/demoBackend/src/main/resources/static/css/main.css index 31f3ebe..1b6bc96 100644 --- a/demoBackend/src/main/resources/static/css/main.css +++ b/demoBackend/src/main/resources/static/css/main.css @@ -1,5 +1,10 @@ html { - font-size: 4vw; + font-size: 2vh; +} + +.navbar { + padding-left: 1rem; + padding-right: 1rem; } .cont { diff --git a/demoBackend/src/main/resources/templates/signature.html b/demoBackend/src/main/resources/templates/signature.html index 0c8ce1c..5c4bd12 100644 --- a/demoBackend/src/main/resources/templates/signature.html +++ b/demoBackend/src/main/resources/templates/signature.html @@ -3,38 +3,31 @@ 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.

+

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.
-
-
- - -
+
+
From a4caf24a35d94a3ea72433290f2e5827767ffa81 Mon Sep 17 00:00:00 2001 From: Henrik Lepson Date: Wed, 17 Nov 2021 09:15:29 +0200 Subject: [PATCH 16/18] MOB-41 fixed some remaining issues --- TestMobileApp/README.md | 0 .../com/example/testmobileapp/MainActivity.kt | 36 ++++++++++++++----- .../app/src/main/res/layout/activity_main.xml | 11 ++---- .../app/src/main/res/values-en/strings.xml | 3 +- .../app/src/main/res/values-et/strings.xml | 3 +- .../app/src/main/res/values/strings.xml | 3 +- 6 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 TestMobileApp/README.md diff --git a/TestMobileApp/README.md b/TestMobileApp/README.md new file mode 100644 index 0000000..e69de29 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 d317f18..afc6035 100644 --- a/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt +++ b/TestMobileApp/app/src/main/java/com/example/testmobileapp/MainActivity.kt @@ -11,6 +11,12 @@ 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. @@ -32,6 +38,24 @@ 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) @@ -64,9 +88,7 @@ class MainActivity : AppCompatActivity() { */ private fun getData() { // Enter the server endpoint address to here - //val originUrl = "enter-base-url-here" - val originUrl = "https://5d0c-85-253-195-195.ngrok.io" - val url = "$originUrl/auth/challenge" + val url = "$BASE_URL/auth/challenge" Ion.getDefault(this).conscryptMiddleware.enable(false) Ion.with(applicationContext) .load(url) @@ -76,7 +98,7 @@ class MainActivity : AppCompatActivity() { // Get data from the result and call launchAuth method val challenge = result.asJsonObject["nonce"].toString().replace("\"", "") Log.v("Challenge", challenge) - launchAuth(challenge, originUrl, "/auth/authentication") + launchAuth(challenge, BASE_URL, "/auth/authentication") } catch (e: Exception) { Log.i("GETrequest", "was unsuccessful") } @@ -87,14 +109,12 @@ class MainActivity : AppCompatActivity() { binding.loginOptions.visibility = View.VISIBLE } - private fun showResult(resultObject: String, token: String) { + private fun showResult(user: String) { binding.loginOptions.visibility = View.GONE binding.resultLayout.visibility = View.VISIBLE - binding.resultObject.text = resultObject - binding.resultToken.text = token + binding.resultObject.text = getString(R.string.hello, user) binding.buttonForget.setOnClickListener { binding.resultObject.text = "" - binding.resultToken.text = "" binding.resultLayout.visibility = View.GONE binding.loginOptions.visibility = View.VISIBLE } diff --git a/TestMobileApp/app/src/main/res/layout/activity_main.xml b/TestMobileApp/app/src/main/res/layout/activity_main.xml index 98727e4..dab6a25 100644 --- a/TestMobileApp/app/src/main/res/layout/activity_main.xml +++ b/TestMobileApp/app/src/main/res/layout/activity_main.xml @@ -52,7 +52,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toBottomOf="@id/login_text_view" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:visibility="gone"> @@ -64,14 +64,7 @@ android:layout_margin="6dp" android:textSize="18sp"/> - - - Login Choose login method NFC auth - Successful response + Logged in Response failed Forget + Hello, %s! \ No newline at end of file diff --git a/TestMobileApp/app/src/main/res/values-et/strings.xml b/TestMobileApp/app/src/main/res/values-et/strings.xml index 09391e1..d4f3981 100644 --- a/TestMobileApp/app/src/main/res/values-et/strings.xml +++ b/TestMobileApp/app/src/main/res/values-et/strings.xml @@ -4,7 +4,8 @@ Logi sisse Vali sobiv meetod NFC auth - Vastus kätte saadud + Sisse logimine õnnestus Vastust ei õnnestunud kätte saada Unusta + Tere, %s! \ No newline at end of file diff --git a/TestMobileApp/app/src/main/res/values/strings.xml b/TestMobileApp/app/src/main/res/values/strings.xml index 6108518..b340b7e 100644 --- a/TestMobileApp/app/src/main/res/values/strings.xml +++ b/TestMobileApp/app/src/main/res/values/strings.xml @@ -3,7 +3,8 @@ Login Choose login method NFC auth - Successful response + Logged in Response failed Forget + Hello, %s! \ No newline at end of file From 68a7db2e77d15a2cd604eb2fe02ab575bb9ce4ae Mon Sep 17 00:00:00 2001 From: Henrik Lepson <56916788+Henrik895@users.noreply.github.com> Date: Wed, 17 Nov 2021 09:26:21 +0200 Subject: [PATCH 17/18] Created a readme for TestMobileApp --- TestMobileApp/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/TestMobileApp/README.md b/TestMobileApp/README.md index e69de29..e84ab0d 100644 --- a/TestMobileApp/README.md +++ 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. From 2c5430977d645453995c1117fcecaa67bed9106c Mon Sep 17 00:00:00 2001 From: Henrik Lepson <56916788+Henrik895@users.noreply.github.com> Date: Wed, 17 Nov 2021 09:31:30 +0200 Subject: [PATCH 18/18] Updated main readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) 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*