Quarkus - How to send a secret from Mobile to Server?

Hello everyone, Today we will learn how to implement end-to-end encryption in the Quarkus ecosystem. Here I am showing architecture visualization and demo application implementation.
  • Encryption's primary purpose is to protect against brute force attacks. It is composed of a cipher, the message/data, and a key (the password). With a wide range of free tools available like(Aircrack-ng, John the Ripper, Rainbow Crack, L0phtCrack ), even baby hackers can attempt to hack passwords using brute force.
  • In my opinion, as a layperson in cryptography, multiple double-layer encryptions may not increase security, but they may slow down attackers.
  • Using encryption may cause performance issues. Or maybe not. It really depends on how you use it. If you understand just how "expensive" each part of your enterprise encryption operation is, it's possible you can avoid the expensive parts and dramatically increase the performance of your applications.

Let's see the architecture of the end to end encryption 

*We are using RSA and AES algorithm to implement double-layer security*


Here, the client will request two API calls to Server.

Request 1:
  1. Client-side (Android or ios) will generate RSA private key and public key.
  2. The client will store the Private key on local storage. Then, the client will send a Public key to the Quarkus Backend.
  3. On the Quarkus Backend, a secure AES key will be generated and store that key on local temporary storage.
  4. On the Quarkus Backend, encrypt the AES key using RSA Public key which has already been received from the Client-side over an HTTP request.
  5. Then send encrypted AES key to the client over HTTP response.
Request 2:
    1. The client will receive an encrypted AES key from Server, which I already mentioned above.
    2. On the client-side, it will decrypt the AES key using RSA private key which is already stored in client local storage.
    3. Using that AES key, the client will encrypt the secrets like PIN, PASSWORD or whatever.
    4. And send that encrypted secret to Quarkus Backend.
    5. On the Quarkus backend side, it will decrypt the secret using the AES key which is already stored in local temporary storage.
    6. And the Quarkus backend will send the success response to the Client.

    Implementation

    *Important*
    "On the backend side, I am using Quarkus.
    The Frontend side is not real for time being, I just mocked the data from the backend."

    Project Structure:

    Pom.xml - Dependency management

    <?xml version="1.0"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
    https://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.knf.dev.demo</groupId>
    <artifactId>quarku-end-to-to-enc-dec</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
    <compiler-plugin.version>3.8.1</compiler-plugin.version>
    <maven.compiler.parameters>true</maven.compiler.parameters>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <quarkus-plugin.version>2.2.3.Final</quarkus-plugin.version>
    <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
    <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
    <quarkus.platform.version>2.2.3.Final</quarkus.platform.version>
    <surefire-plugin.version>2.22.1</surefire-plugin.version>
    </properties>
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>${quarkus.platform.group-id}</groupId>
    <artifactId>${quarkus.platform.artifact-id}</artifactId>
    <version>${quarkus.platform.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm</artifactId>
    </dependency>
    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-h2</artifactId>
    </dependency>
    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jsonb</artifactId>
    </dependency>
    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy</artifactId>
    </dependency>
    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    </dependency>
    </dependencies>
    <build>
    <plugins>
    <plugin>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-maven-plugin</artifactId>
    <version>${quarkus-plugin.version}</version>
    <executions>
    <execution>
    <goals>
    <goal>build</goal>
    </goals>
    </execution>
    </executions>
    </plugin>
    <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    </plugin>
    <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <configuration>
    <systemProperties>
    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
    </systemProperties>
    </configuration>
    </plugin>
    </plugins>
    </build>

    <profiles>
    <profile>
    <id>native</id>
    <activation>
    <property>
    <name>native</name>
    </property>
    </activation>
    <build>
    <plugins>
    <plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <executions>
    <execution>
    <goals>
    <goal>integration-test</goal>
    <goal>verify</goal>
    </goals>
    <configuration>
    <systemProperties>
    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
    </systemProperties>
    </configuration>
    </execution>
    </executions>
    </plugin>
    </plugins>
    </build>
    <properties>
    <quarkus.package.type>native</quarkus.package.type>
    </properties>
    </profile>
    </profiles>
    </project>

    Data transfer Object:

    package org.knf.dev.demo.model;

    public class User {
    private String pin;
    private String email;
    private String rsaPublicKey;
    private String rsaPrivateKey;
    private String aesKey;
    private String secret;

    public String getSecret() {
    return secret;
    }

    public void setSecret(String secret) {
    this.secret = secret;
    }

    public String getAesKey() {
    return aesKey;
    }

    public void setAesKey(String aesKey) {
    this.aesKey = aesKey;
    }

    public String getPin() {
    return pin;
    }

    public void setPin(String pin) {
    this.pin = pin;
    }

    public String getEmail() {
    return email;
    }

    public void setEmail(String email) {
    this.email = email;
    }

    public String getRsaPublicKey() {
    return rsaPublicKey;
    }

    public void setRsaPublicKey(String rsaPublicKey) {
    this.rsaPublicKey = rsaPublicKey;
    }

    public String getRsaPrivateKey() {
    return rsaPrivateKey;
    }

    public void setRsaPrivateKey(String rsaPrivateKey) {
    this.rsaPrivateKey = rsaPrivateKey;
    }
    }

    application.properties:

    # datasource configuration
    quarkus.datasource.db-kind = h2
    quarkus.datasource.username = sa
    # quarkus.datasource.password =
    quarkus.datasource.jdbc.url = jdbc:h2:mem:test
    # drop and create the database at startup (use `update` to only update the
    schema)
    quarkus.hibernate-orm.database.generation=drop-and-create

    Backend:

    Create an Entity class:

    Table 'backend' will act like a cache, which will store keys and values.
    package org.knf.dev.demo.backend.entity;

    import javax.persistence.*;
    import java.io.Serializable;

    @Table(name = "backend")
    @Entity

    public class BackEndTempStorage implements Serializable {
    @Id
    private String key;

    @Column(length=2500)
    private String value;

    public String getKey() {
    return key;
    }

    public void setKey(String key) {
    this.key = key;
    }

    public String getValue() {
    return value;
    }

    public void setValue(String value) {
    this.value = value;
    }

    public BackEndTempStorage(String key, String value) {
    this.key = key;
    this.value = value;
    }

    public BackEndTempStorage() {
    }

    }

    Repository:

    package org.knf.dev.demo.backend.entity;

    import javax.inject.Inject;
    import javax.inject.Singleton;
    import javax.persistence.EntityManager;
    import javax.transaction.Transactional;

    @Singleton
    public class BackEndTempStorageService {

    @Inject
    EntityManager entityManager;

    public BackEndTempStorage get(String id) {
    return entityManager.find(BackEndTempStorage.class, id);
    }

    @Transactional(Transactional.TxType.REQUIRED)
    public BackEndTempStorage save(BackEndTempStorage data) {
    entityManager.persist(data);
    return data;
    }

    @Transactional(Transactional.TxType.REQUIRED)
    public void delete(String id) {
    BackEndTempStorage user = get(id);
    entityManager.remove(user);
    }
    }

    Create a crypto Service class:

    package org.knf.dev.demo.backend;

    import javax.crypto.Cipher;
    import javax.crypto.spec.SecretKeySpec;
    import javax.enterprise.context.ApplicationScoped;
    import java.io.UnsupportedEncodingException;
    import java.nio.charset.StandardCharsets;
    import java.security.*;
    import java.security.spec.InvalidKeySpecException;
    import java.security.spec.X509EncodedKeySpec;
    import java.util.Arrays;
    import java.util.Base64;

    /*
    @BackEndWebSecurityService is our backend Resource
    End Point
    */
    @ApplicationScoped
    public class BackEndWebSecurityService {
    private static byte[] key;
    private static SecretKeySpec secretKey;

    // set Key
    public static void setKey(String myKey) {
    MessageDigest sha = null;
    try {
    key = myKey.getBytes(StandardCharsets.UTF_8);
    sha = MessageDigest.getInstance("SHA-1");
    key = sha.digest(key);
    key = Arrays.copyOf(key, 32);
    secretKey = new SecretKeySpec(key, "AES");
    } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    }
    }

    public static String secureRandomString() {
    SecureRandom random = new SecureRandom();
    byte[] bytes = new byte[20];
    random.nextBytes(bytes);
    Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
    String token = encoder.encodeToString(bytes);
    return token;
    }

    /*
    Encrypt AES Key using RSA public key
    */
    public static String encryptAESUsingPublicKey(String AESKey,
    String publicKey) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, convertToPublicKey(publicKey));
    return Base64.getEncoder().encodeToString(cipher.doFinal
    (AESKey.getBytes()));
    }

    /*
    Convert Public Key String to Public Key Object
    */
    public static PublicKey convertToPublicKey(String pKey)
    throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] publicKeyByteServer = Base64.getDecoder().decode(pKey);
    // generate the publicKey
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PublicKey publicKey = keyFactory.
    generatePublic(new X509EncodedKeySpec(publicKeyByteServer));
    return publicKey;
    }

    // method to encrypt the secret text using key
    public static String decrypt(String strToDecrypt, String secret) {
    try {
    setKey(secret);
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
    } catch (Exception e) {
    System.out.println("Error while decrypting: " + e);
    }
    return null;
    }
    }

    Create Crypto Endpoint:

    package org.knf.dev.demo.backend;

    import org.knf.dev.demo.backend.entity.BackEndTempStorage;
    import org.knf.dev.demo.backend.entity.BackEndTempStorageService;
    import org.knf.dev.demo.model.User;
    import javax.inject.Inject;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    import javax.ws.rs.core.Response;

    @Path("/backend/api/v1")
    public class BackEndSecretResource {
    @Inject
    BackEndWebSecurityService backEndWebSecurityService;

    @Inject
    BackEndTempStorageService tempStorageService;

    /*
    * @Back end
    * 1. Encrypt the system generate AES Key using public key
    * from Client (Mobile)
    * 2. send encrypted AES key to Client(Mobile App)
    */
    @POST
    @Path("/getAES")
    public Response getAES(User user) throws Exception {

    //Genearte AES Key
    String aesKey = backEndWebSecurityService.secureRandomString();

    //Encrypt AES Key using RSA Public key from Mobile
    String encryptedAESKey = backEndWebSecurityService.
    encryptAESUsingPublicKey(aesKey, user.getRsaPublicKey());
    user.setAesKey(encryptedAESKey);

    //Store Aes key
    tempStorageService.save(new BackEndTempStorage(user.getEmail(), aesKey));
    user.setEmail(null);
    user.setRsaPublicKey(null);
    return Response
    .status(Response.Status.OK)
    .entity(user)
    .build();

    }

    @POST
    @Path("/verify/decryption")
    /*
    * @Backend
    * Verify decryption
    */
    public Response success(User user) throws Exception {

    BackEndTempStorage storage = tempStorageService.get(user.getEmail());
    String pin = BackEndWebSecurityService.decrypt(user.getSecret(), storage.getValue());
    user.setPin(pin);
    user.setAesKey(null);
    user.setEmail(null);
    user.setSecret(null);
    return Response
    .status(Response.Status.OK)
    .entity(user)
    .build();

    }
    }

    Simulator- Front End:

    Create an Entity class:

    Table 'mobile' will act like a cache, which will store keys and values.

    package org.knf.dev.demo.mobilewebsimulator.entity;

    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import java.io.Serializable;

    @Table(name = "mobile")
    @Entity
    public class MobileTempStorage implements Serializable {
    @Id
    private String key;

    @Column(length=2500)
    private String value;

    public String getKey() {
    return key;
    }

    public void setKey(String key) {
    this.key = key;
    }

    public String getValue() {
    return value;
    }

    public void setValue(String value) {
    this.value = value;
    }

    public MobileTempStorage(String key, String value) {
    this.key = key;
    this.value = value;
    }

    public MobileTempStorage() {
    }

    }

    Repository:

    package org.knf.dev.demo.mobilewebsimulator.entity;

    import javax.inject.Inject;
    import javax.inject.Singleton;
    import javax.persistence.EntityManager;
    import javax.transaction.Transactional;

    @Singleton
    public class MobileTempStorageService {

    @Inject
    EntityManager entityManager;

    public MobileTempStorage get(String id) {
    return entityManager.find(MobileTempStorage.class, id);
    }

    @Transactional(Transactional.TxType.REQUIRED)
    public MobileTempStorage save(MobileTempStorage data) {
    entityManager.persist(data);
    return data;
    }

    @Transactional(Transactional.TxType.REQUIRED)
    public void delete(String id) {
    MobileTempStorage user = get(id);
    entityManager.remove(user);
    }
    }

    Mobile Crypto Service class:

    package org.knf.dev.demo.mobilewebsimulator;

    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.spec.SecretKeySpec;
    import javax.enterprise.context.ApplicationScoped;
    import java.io.UnsupportedEncodingException;
    import java.nio.charset.StandardCharsets;
    import java.security.*;
    import java.security.spec.PKCS8EncodedKeySpec;
    import java.util.Arrays;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.Map;

    @ApplicationScoped
    public class MobileWebSecurityService {

    private static SecretKeySpec secretKey;
    private static byte[] key;


    public static void setKey(String myKey) {
    MessageDigest sha = null;
    try {
    key = myKey.getBytes(StandardCharsets.UTF_8);
    sha = MessageDigest.getInstance("SHA-1");
    key = sha.digest(key);
    key = Arrays.copyOf(key, 32);
    secretKey = new SecretKeySpec(key, "AES");
    } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    }
    }

    // Get RSA keys. Uses key size of 2048.
    public static Map<String, Object> getRSAKeys() throws Exception {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(1024);
    KeyPair keyPair = keyPairGenerator.generateKeyPair();
    PrivateKey privateKey = keyPair.getPrivate();
    PublicKey publicKey = keyPair.getPublic();
    Map<String, Object> keys = new HashMap<String, Object>();
    keys.put("private", privateKey);
    keys.put("public", publicKey);
    return keys;
    }

    //Public key to String
    public static String publicKeyToString(PublicKey publicKey) {
    byte[] byte_pubkey = publicKey.getEncoded();
    //converting byte to String
    String str_key = Base64.getEncoder().encodeToString(byte_pubkey);
    // String str_key = new String(byte_pubkey,Charset.);
    return str_key;
    }

    //Private key to String
    public static String privateKeyToString(PrivateKey privateKey) {
    byte[] byte_pubkey = privateKey.getEncoded();
    //converting byte to String
    String str_key = Base64.getEncoder().encodeToString(byte_pubkey);
    // String str_key = new String(byte_pubkey,Charset.);
    return str_key;
    }


    // Decrypt encrypted AES Key using RSA Key which already stored
    public static String decryptMessageUsingPrivateKey(String encryptedText,
    String privateKey)
    throws Exception {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    cipher.init(Cipher.DECRYPT_MODE, loadPrivateKey(privateKey));
    return new String(cipher.doFinal(Base64.getDecoder().
    decode(encryptedText)));
    }

    // Convert String private key to privateKey object
    public static PrivateKey loadPrivateKey(String key64)
    throws GeneralSecurityException {
    byte[] clear = Base64.getDecoder().decode((key64.getBytes()));
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(clear);
    KeyFactory fact = KeyFactory.getInstance("RSA");
    PrivateKey priv = fact.generatePrivate(keySpec);
    Arrays.fill(clear, (byte) 0);
    return priv;
    }

    //Encrypt Pin/Password/other secrets using AES Key
    public String encryptPin(String pin, String aesKey)
    throws NoSuchPaddingException, NoSuchAlgorithmException,
    InvalidKeyException, UnsupportedEncodingException,
    IllegalBlockSizeException, BadPaddingException {
    setKey(aesKey);
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    return Base64.getEncoder().encodeToString(cipher.doFinal
    (pin.getBytes(StandardCharsets.UTF_8)));


    }
    }

    Mobile Crypto Endpoints:

    package org.knf.dev.demo.mobilewebsimulator;

    import org.knf.dev.demo.mobilewebsimulator.entity.MobileTempStorage;
    import org.knf.dev.demo.mobilewebsimulator.entity.MobileTempStorageService;
    import org.knf.dev.demo.model.User;
    import javax.inject.Inject;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    import javax.ws.rs.PathParam;
    import javax.ws.rs.core.Response;
    import java.security.PrivateKey;
    import java.security.PublicKey;
    import java.util.Map;

    /*
    @MobileWebSecretResource will act like Android/ios or Web Apps
    End Point
    */
    @Path("/simulator/v1")
    public class MobileWebSecretResource {

    @Inject
    MobileWebSecurityService mobileWebSecurityService;

    @Inject
    MobileTempStorageService tempStorageService;

    /*
    * @Client: Generate Public and Private Key
    * And Sent public key to the server and store the private key on
    * Client local storage for future use
    */
    @POST
    @Path("/generateKeys/{emailId}")
    public Response generateKeys(@PathParam("emailId") String emailId) throws Exception {

    User user = new User();
    user.setEmail(emailId);

    /*
    * @Client: Generate Public and Private Key
    */
    Map<String, Object> keys = mobileWebSecurityService.getRSAKeys();
    PrivateKey privateKey = (PrivateKey) keys.get("private");
    PublicKey publicKey = (PublicKey) keys.get("public");

    /*
    * @Client: Sent public key to the server
    */
    String publicKeyToString = mobileWebSecurityService.publicKeyToString(publicKey);
    user.setRsaPublicKey(publicKeyToString);


    /*
    *store the private key
    */
    String privateKeyToString = mobileWebSecurityService.privateKeyToString(privateKey);
    tempStorageService.save(new MobileTempStorage(user.getEmail(), privateKeyToString));

    return Response
    .status(Response.Status.OK)
    .entity(user)
    .build();
    }

    @POST
    @Path("/verify/encryption")
    /*
    A stimulator to verify encryption.
    */
    public Response encrypt(User user) throws Exception {

    String encryptedAESKey = user.getAesKey();
    MobileTempStorage privateKey = tempStorageService.get(user.getEmail());
    //Set Private RSA Key
    user.setEmail(privateKey.getKey());
    //Convert enc AES Key to Text
    String aesKey = MobileWebSecurityService.
    decryptMessageUsingPrivateKey(encryptedAESKey, privateKey.getValue());
    String secretPin = "8543";
    String encryptedPin = mobileWebSecurityService.encryptPin(secretPin, aesKey);
    user.setPin(encryptedPin);
    user.setRsaPublicKey(null);
    user.setAesKey(null);
    user.setRsaPrivateKey(null);
    return Response
    .status(Response.Status.OK)
    .entity(user)
    .build();
    }
    }

    Demo:

    Run the Quarkus Application

    Build application  jar file: mvn clean package

    Start the application: java -jar quarkus-run.jar

    Verify APIs:

    Step 1:  Simulate RSA public key and private key

    Step 2: HTTP request to the server along with the public key

    Step 3: Encrypt the Pin on mobile 

    Step 4: HTTP request to the server with encrypted secret



    More Quarkus topics,


    Popular posts from this blog

    Learn Java 8 streams with an example - print odd/even numbers from Array and List

    Java Stream API - How to convert List of objects to another List of objects using Java streams?

    Registration and Login with Spring Boot + Spring Security + Thymeleaf

    Java, Spring Boot Mini Project - Library Management System - Download

    ReactJS, Spring Boot JWT Authentication Example

    Spring Boot + Mockito simple application with 100% code coverage

    Top 5 Java ORM tools - 2024

    Java - Blowfish Encryption and decryption Example

    Spring boot video streaming example-HTML5

    Google Cloud Storage + Spring Boot - File Upload, Download, and Delete