/ React

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.

secure_transaction

How to send a secret from React to Java Spring?
Share this