Search code examples
javaencryptionaesrsapublic-key-exchange

java.security.InvalidKeyException thrown during Java AES key exchange using RSA


I'm trying to make a client/server program in Java that allows the server to send messages encrypted using AES to the client. Right now, I'm having problems while creating the key exchange protocol. The way that this key exchange current works is:

  1. Client generates RSA public/private key pair
  2. Client sends out his RSA public key to the server
  3. Server generates and encrypts AES key with client's RSA public key
  4. Server sends encrypted AES key to client
  5. Both parties now have the correct AES key and all messages can be encrypted using AES

However, every time I get to step three, I am unable to encrypt the generated AES key with the client's RSA public key because I get this error:

java.security.InvalidKeyException: No installed provider supports this key: javax.crypto.spec.SecretKeySpec
    at java.base/javax.crypto.Cipher.chooseProvider(Cipher.java:919)
    at java.base/javax.crypto.Cipher.init(Cipher.java:1275)
    at java.base/javax.crypto.Cipher.init(Cipher.java:1212)
    at test.Server.<init>(Server.java:50)
    at test.Start.main(Start.java:11)

As a result, I am unable to complete the AES key exchange that I'm trying to do.

Server.java is used to do things on the server side, and Client.java is used to do everything on the client side. My Server.java file looks like this:

public class Server {
    private ServerSocket serverSocket; // Server socket
    private Socket socket; // Socket
    private BufferedReader in; // Reading from stream
    private PrintWriter out; // Writing to stream
    private Key key; // AES key used for encryption

    // Constructor
    public Server() {
        // Initialize the server socket
        try {
            // Setup connections
            serverSocket = new ServerSocket(12345);
            socket = serverSocket.accept();
            out = new PrintWriter(socket.getOutputStream(), true);
            in = new BufferedReader(newInputStreamReader(socket.getInputStream()));

            // Receive the client's public RSA key
            byte[] encodedClientKey = Base64.getDecoder().decode(in.readLine());
            Key clientRSAKey = new SecretKeySpec(encodedClientKey, 0, encodedClientKey.length, "RSA");

            // Generate AES key
            KeyGenerator aesKeyGen = KeyGenerator.getInstance("AES");
            aesKeyGen.init(256);
            key = aesKeyGen.generateKey();

            // Encrypt the AES key using the client's RSA public key
            Cipher c = Cipher.getInstance("RSA");
            c.init(Cipher.ENCRYPT_MODE, clientRSAKey);
            byte[] encryptedAESKey = c.doFinal(key.getEncoded());

            // Send the encrypted AES key to the client
            sendUnencrypted(Base64.getEncoder().encodeToString(encryptedAESKey));
        } catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
            | IllegalBlockSizeException | BadPaddingException e) {
            e.printStackTrace();
        }
    }

    // Receive an unencrypted message
    public String receiveUnencrypted() {
        try {
            // Wait until the stream is ready to be read
            while (true)
                if (in.ready())
                    break;

            return in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // Send an unencrypted message
    public void sendUnencrypted(String message) {
        out.println(message);
        out.flush();
    }

    // Send an encrypted message
    public void send(String message) {
        try {
            // Encrypt the message
            Cipher c = Cipher.getInstance("AES");
            c.init(Cipher.ENCRYPT_MODE, key);
            String encoded = Base64.getEncoder().encodeToString(message.getBytes("utf-8"));
            byte[] encrypted = c.doFinal(encoded.getBytes());
            String encryptedString = Base64.getEncoder().encodeToString(encrypted);

            // Send the encrypted message
            out.println(encryptedString);
            out.flush();
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
            | BadPaddingException | UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}

My Client.java file looks like this:

public class Client {
    private Socket socket; // Socket
    private BufferedReader in; // Reading from stream
    private PrintWriter out; // Writing to stream
    private Key key; // AES key

    // Constructor
    public Client() {
        try {
            // Create streams to server
            socket = new Socket("127.0.0.1", 12345);
            out = new PrintWriter(socket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // Generate an RSA key pair
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(2048);
            KeyPair kp = keyGen.generateKeyPair();

            // Send out our public key to the server
            byte[] publicKey = kp.getPublic().getEncoded();
            sendUnencrypted(Base64.getEncoder().encodeToString(publicKey));

            // Recieve and decrypt the AES key sent from the server
            String encryptedKey = receiveUnencrypted();
            Cipher c = Cipher.getInstance("RSA");
            c.init(Cipher.DECRYPT_MODE, kp.getPrivate());
            byte[] AESKey = c.doFinal(encryptedKey.getBytes());
            key = new SecretKeySpec(AESKey, 0, AESKey.length, "AES");
        } catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
                | IllegalBlockSizeException | BadPaddingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    // Receive an unencrypted message
    public String receiveUnencrypted() {
        try {
            // Wait until the stream is ready to be read
            while (true)
                if (in.ready())
                    break;

            return in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // Send an unencrypted message
    public void sendUnencrypted(String message) {
        out.println(message);
        out.flush();
    }

    // Receive an encrypted message
    public String receive() {
        try {
            // Wait until the stream is ready to be read
            while (true)
                if (in.ready())
                    break;

            // Obtain the encrypted message
            String encrypted = in.readLine();

            // Decrypt and return the message
            Cipher c = Cipher.getInstance("AES");
            c.init(Cipher.DECRYPT_MODE, key);
            byte[] decoded = Base64.getDecoder().decode(encrypted);
            String utf8 = new String(c.doFinal(decoded));
            String plaintext = new String(Base64.getDecoder().decode(utf8));

            // Return the message
            return plaintext;
        } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
                | IllegalBlockSizeException | BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Start.java is used to initial both the server and client.

package test;

import java.util.Scanner;

public class Start {
    public static void main(String args[]) {
        Scanner scan = new Scanner(System.in);
        System.out.println("1.) Create data server.\n2.) Connect to data server.\nPlease select an option: ");
        int option = scan.nextInt();
        if (option == 1) {  // Setup a server if they choose option one
            Server s = new Server();
            s.send("Hello");
        } else if (option == 2) {  // Setup a client if they choose option two
            Client c = new Client();
            System.out.println(c.receive());
        }

        // Close scanner
        scan.close();
    }
}

Solution

  • First, you can't use SecretKeySpec to restore an RSA public key. In your Server's constructor, change

    Key clientRSAKey = new SecretKeySpec(encodedClientKey, 0, encodedClientKey.length, "RSA");
    

    to

    Key clientRSAKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(encodedClientKey));
    

    Second, you need to decode the base64 encoded encrypted key. In your Client constructor, change

    String encryptedKey = receiveUnencrypted();
    

    to

    byte[] encryptedKey = Base64.getDecoder().decode(receiveUnencrypted());
    

    Finally, in your Client constructor, change

    byte[] AESKey = c.doFinal(encryptedKey.getBytes());
    

    to

    byte[] AESKey = c.doFinal(encryptedKey);