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:
- Client-side (Android or ios) will generate RSA private key and public key.
- The client will store the Private key on local storage. Then, the client will send a Public key to the Quarkus Backend.
- On the Quarkus Backend, a secure AES key will be generated and store that key on local temporary storage.
- 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.
- Then send encrypted AES key to the client over HTTP response.
Request 2:
- The client will receive an encrypted AES key from Server, which I already mentioned above.
- On the client-side, it will decrypt the AES key using RSA private key which is already stored in client local storage.
- Using that AES key, the client will encrypt the secrets like PIN, PASSWORD or whatever.
- And send that encrypted secret to Quarkus Backend.
- On the Quarkus backend side, it will decrypt the secret using the AES key which is already stored in local temporary storage.
- 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
Download the source code: https://github.com/knowledgefactory4u/Quarkus
More Quarkus topics,