How to send a secret from React to Java Spring?
4youngpadawans.com presents How to send a secret from React to Java Spring? featuring React | Spring
During violent history of humankind it was very important to pass a message to fellow army on such a way that enemy cannot intercept it and read it. If enemy caught messenger carrying written note (in plain text) with instructions, plans or tactics, odds to loose the battle were increasing drastically.
First recorded attempt in history to encrypt/encode/cipher messages is "officially" granted to Julius Ceasar, emperor of once powerful but long gone Roman empire (Ceasar cipher). And that was just a beginning...
War. War never changes..
Nowadays battles are mostly (and very unfortunately not entirely) shifted to a modern, digital battleground called Internet, where endless number of clients are fighting to send/receive messages to/from endless number of servers.
Since digital messages travel a long way from source to destination usually through pirate infested waters of public Internet, securely exchanging sensitive information between client and server is essential: message sender needs to encrypt message on such a way that only intended recipient can decrypt it. This kind of communication encoding is called end-to-end encryption.
In this article, I will explore end-to-end encryption with React client and Java Spring API server.
Use case
Let's suppose that we've just got following requirement:
- we need to build a web page where user will input payment transaction information including credit card information and post it to server,
- credit card and transaction sensitive information needs to be sent using secure way so that nobody (but our server) can decode it.
Implementation
Let's start with introducing cryptography system known as asymmetric cryptography where pair of cryptography keys is used to encrypt and decrypt information:
- public key - can be distributed publicly and is used to encrypt information,
- private key - should be kept as secret and is used to decrypt information encrypted with private key
Since asymmetric algorithms are considered too slow, require longer keys to achieve better security and have limitations in case of larger amounts of data, we will not use asymmetric algorithm to encrypt transaction data but only to encrypt secret (primary) key for symmetric cryptography algorithm actually used for transaction encryption.
In following examples we will use
- asymmetric RSA algorithm with 1024 bit key size to encrypt secret key for symmetric algorithm,
- symmetric AES algorithm with 128 bit key size to encrypt data.
Work flow will be:
- client prepares data for transaction and then asks server for RSA public key,
- server generates RSA key pair (public and private key) and shares RSA public key with client,
- client generates AES secret key and encrypts transaction with it,
- client encrypts AES secret key with RSA public key,
- client sends AES encrypted transaction and RSA encrypted secret key to server,
- server decrypts AES secret key with RSA private key,
- server decrypts transaction using decrypted AES secret key.
Models
First lets define models for our system.
User.java
containing basic information about system user executing payment transaction:
package com.dmi.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
public class User {
private String id;
private String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String rsaPublicKey;
//we do not want to expose private RSA key in API responses
@JsonIgnore
private String rsaPrivateKey;
//we do not want to expose AES primary key in API responses
@JsonIgnore
private String aesKey;
public User(String id, String name) {
this.id = id;
this.name = name;
}
//getters and setters
}
CreditCard.java
with credit card information:
package com.dmi.model;
public class CreditCard {
private String number;
private String securityCode;
//..other card related info
//getters and setters..
}
Transaction.java
with all necessary information about payment transaction:
package com.dmi.model;
public class Transaction {
private String userId;
private String type;
private Double amount;
//some other transaction properties
//...
private CreditCard creditCard;
//getters and setters...
}
EncryptedTransaction.java
that will actually be sent from client to server:
package com.dmi.model;
public class EncryptedTransaction {
String userId;
//AES encrypted transaction information
String payload;
//RSA encypted AES key
String encAesKey;
//getters and setters...
}
Generating RSA key pair - server side
Java API server needs to be capable to generate RSA key pair - private and public key and to provide endpoint so that client can get public key.
CryptoRestController.java
@Controller
@RequestMapping(path = "/crypto")
public class CryptoRestController {
@Autowired
UserService userService;
@CrossOrigin
@GetMapping(path = "/publickey")
public @ResponseBody User getPublicKey(@RequestParam(value = "userId") String userId) throws Exception {
User user = userService.getUser(userId);
//server generates RSA key pair - public and private keys
generateRsaKeyPair(user);
userService.updateUser(user);
//to simplify our example, User object is returned with generated RSA public key
//RSA private key is not included in response because it should be kept as secret
return user;
}
private void generateRsaKeyPair(User user) throws NoSuchAlgorithmException {
KeyPair keyPair = CryptoUtil.generateRsaKeyPair();
byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded();
//encoding keys to Base64 text format so that we can send public key via REST API
String rsaPublicKeyBase64 = new String(Base64.getEncoder().encode(publicKey));
String rsaPrivateKeyBase64 = new String(Base64.getEncoder().encode(privateKey));
//saving keys to user object for later use
user.setRsaPublicKey(rsaPublicKeyBase64);
user.setRsaPrivateKey(rsaPrivateKeyBase64);
}
//...
}
Encrypting transaction - client side
In order to implement flow we defined above, we need JavaScript libraries for cryptographic standards. For this purpose we will use crypto-js for AES and encryptjs for RSA algorithm.
transaction-encryption.jsx
import React, { Component } from 'react';
import CryptoJS from 'crypto-js';
import JSEncrypt from 'jsencrypt';
import { CryptoService } from '../services/crypto-service.jsx';
//import other components here
export class TransactionEncryption extends Component {
constructor() {
super();
this.cryptoservice = new CryptoService();
this.user = { userId: 'd41cd772-cb57-412c-a864-6e40b2bd3e12' };
this.state = { type: 'payment', amount: 100, number: '123-456-789-000', securitycode: '123' ,log:''};
}
doTransaction = () => {
this.cryptoservice.getUserPublicKey(this.user)
.then((resUser) => {
this.log("RSA public key[base64]: " + resUser.data.rsaPublicKey);
let transaction = { type: this.state.type, amount: this.state.amount, creditcard: { number: this.state.number, securitycode: this.state.securitycode } };
//generate AES key
var secretPhrase = CryptoJS.lib.WordArray.random(16);
var salt = CryptoJS.lib.WordArray.random(128 / 8);
//aes key 128 bits (16 bytes) long
var aesKey = CryptoJS.PBKDF2(secretPhrase.toString(), salt, {
keySize: 128 / 32
});
//initialization vector - 1st 16 chars of userId
var iv = CryptoJS.enc.Utf8.parse(this.user.userId.slice(0, 16));
var aesOptions = { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv };
var aesEncTrans = CryptoJS.AES.encrypt(JSON.stringify(transaction), aesKey, aesOptions);
this.log(`Transaction: ${JSON.stringify(transaction)}`);
this.log('AES encrypted transaction [Base64]: ' + aesEncTrans.toString());
this.log('AES key [hex]: ' + aesEncTrans.key);
this.log('AES init vector [hex]: ' + aesEncTrans.iv);
//encrypt AES key with RSA public key
var rsaEncrypt = new JSEncrypt();
rsaEncrypt.setPublicKey(resUser.data.rsaPublicKey);
var rsaEncryptedAesKey = rsaEncrypt.encrypt(aesEncTrans.key.toString());
this.log('RSA encrypted AES key [base64]: ' + rsaEncryptedAesKey);
var encryptedTransaction = { userId: this.user.userId, payload: aesEncTrans.toString(), encAesKey: rsaEncryptedAesKey };
this.cryptoservice.doTransaction(encryptedTransaction);
showInfoMessage(this, 'System', 'Secure transaction completed.');
}).catch((error) => {
console.error(error);
});
};
render() {
return (
<div>
//define UI components here
</div >
)
};
}
Pay attention on important parts in code above:
- encryptjs library accepts and produces Base64 encoded strings,
- function
CryptoJS.AES.encrypt
produces complex object but we took only what we needed, - since AES is block algorithm (block size is equal to key size), we used Pkcs7 padding to suffix the last block up to 16 bytes in this case.
- AES initialization vector (IV) is not required to be secret so we used first 16 chars of
userId
to simplify our procedure. Ideal solution would be to randomly generate IV and to pass it to server along with encrypted transaction. - we used 128 bit long AES key but keep in mind: longer the key, harder to crack it using brute-force
Decrypting transaction - server side
Once encrypted transaction is posted to server, we need to decrypt it paying attention on various encodings and, finally, call payment gateway to execute transaction.
CryptoRestController.java
@Controller
@RequestMapping(path = "/crypto")
public class CryptoRestController {
@Autowired
UserService userService;
//...
@CrossOrigin
@PostMapping(path = "/transaction")
public ResponseEntity<?> doTransaction(@RequestBody EncryptedTransaction encryptedTransaction) throws Exception {
User user = userService.getUser(encryptedTransaction.getUserId());
String encAesKeyBase64 = encryptedTransaction.getEncAesKey();
System.out.printf("RSA encrypted Aes Key Base64 [len=%d]: %s\n", encAesKeyBase64.length(),encAesKeyBase64);
//decode from Base64 format
byte[] encAesKeyBytes = Base64.getDecoder().decode(encAesKeyBase64);
System.out.printf("RSA encrypted Aes Key [len=%d]: \n", encAesKeyBytes.length);
//decrypt AES key with private RSA key
byte[] decryptedAesKeyHex =
CryptoUtil.decryptWithPrivateRsaKey(encAesKeyBytes, user.getRsaPrivateKey());
System.out.printf("Decrypted Aes Key [hex]: %s\n ", new String(decryptedAesKeyHex));
byte[] decryptedAesKey = HexUtils.fromHexString(new String(decryptedAesKeyHex));
System.out.printf("Decrypted Aes Key [len=%d]: %s\n", decryptedAesKey.length, new String(decryptedAesKey));
//initialization vector - 1st 16 chars of userId
byte []iv = user.getId().substring(0,16).getBytes();
System.out.printf("Encrypted transaction BASE64 [len=%d]: %s\n", encryptedTransaction.getPayload().length(), encryptedTransaction.getPayload());
byte[] encTransBytes = Base64.getDecoder().decode(encryptedTransaction.getPayload());
//decrypt transaction payload with AES key
byte[] decrypted = CryptoUtil.decryptWithAes(encTransBytes, decryptedAesKey, iv);
System.out.println("Decrypted transaction: " + new String(decrypted));
//cast JSON string to Transaction object
Transaction transaction = new Gson().fromJson(new String(decrypted), Transaction.class);
//for example, call payment gateway with provided information
executeTransaction(user.getId(), transaction);
return ResponseEntity.accepted().build();
}
}
In order to handle cryptographic standards on server side, we used Java Cryptography Extension. Here is handy utility class that wraps up necessary JCE functionalities:
CryptoUtil.java
public class CryptoUtil {
private CryptoUtil() {
}
public static KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
//1024 bit long key
keyGen.initialize(1024);
//generating RSA key pair (public and private)
return keyGen.genKeyPair();
}
public static PrivateKey getRsaPrivateKey(String base64PrivateKey) throws Exception {
byte[] privateKey = Base64.getDecoder().decode(base64PrivateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
public static byte[] decryptWithPrivateRsaKey(byte[] data, String rsaPrivateKeyBase64) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, getRsaPrivateKey(rsaPrivateKeyBase64));
return cipher.doFinal(data);
}
public static byte[] decryptWithAes(byte[] data, byte[] aesKey, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new IvParameterSpec(iv));
return cipher.doFinal(data);
}
}
Note that:
Java Crypto Extension Unlimited Strength should be activated in case we wanted to use AES key longer then 128 bits. Check out this Stackoverflow accepted answer for details.
Conclusion
In examples above I tried to explain how to send an encrypted secret message from React client to Java server utilizing standard cryptographic libraries available for JavaScript and Java programming languages.
Our payment transaction was sent secured enough using RSA and AES algorithms so that it would be very time consuming for man-in-the-middle hacker to decrypt it and find out sensitive details.