Let me explain my problem.
I want to override Socket and ServerSocket classes in order to encrypt my messages in this way:
1) Client sends a random generated symmetric key (AES algorithm) to the Server
2) After that, client and server can communicate by encrypting their messages with this key
3) To exchange the symmetric key the client encrypts it using the public key of the server (RSA algorithm)
I override Socket and ServerSocket, so automatically, when the client opens a Socket, this will send the symmetric key encrypted by the server's public key. The server reads the first 128 byte in the stream, decodes them, and builds the symmetric key.
This part seems work. I check the communication using Wireshark: packets are encrypted and received symmetric key is correctly delivered.
In order to guarantee a transparent use of my Sockets I override the getInputStream and getOutputStream methods, returning a CipheredInputStream and a ChiperedOutputStream.
It doesn't work for now.. When I try to get OutputStream to send data, the program goes through the instruction but it doesn't matter (I check via Wireshark and no packets are sent).
This is the code of the ServerSocket:
public class SecureServerSocket extends ServerSocket {
public SecureServerSocket(int port) throws IOException {
super(port);
}
public Socket accept() throws IOException {
SecureSocket s = new SecureSocket();
implAccept(s);
SecretKey seckey;
InputStream is = s.getInputStream();
byte[] tmp = new byte[128]; //128: length of the key
int i = 0;
while (i < 128) {
tmp[i] = (byte) (is.read() & 0x000000FF);
++i;
}
byte[] mess = EncryptionManager.rsaDecryptPrivate(tmp);
seckey = new SecretKeySpec(mess, "AES");
try {
s.setkey(seckey);
} catch (InvalidKeyException | NoSuchAlgorithmException
| NoSuchPaddingException e) {
e.printStackTrace();
}
return s;
}
}
This is the code of the Socket:
public class SecureSocket extends Socket {
private SecretKey seckey;
private InputStream in = null;
private OutputStream out = null;
private CipherInputStream cin = null;
private CipherOutputStream cout = null;
public SecureSocket() throws IOException {
}
public SecureSocket(String address, int port) throws UnknownHostException,
IOException, NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException {
super(address, port);
if (out == null) {
this.out = super.getOutputStream();
}
if (in == null) {
this.in = super.getInputStream();
}
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
SecureRandom random = new SecureRandom();
keyGen.init(random);
seckey = keyGen.generateKey();
byte[] mess = EncryptionManager.rsaEncryptPublic(seckey.getEncoded());
// writing the initial message with the AES encryption key
out.write(mess);
// Initialization of the Cipher streams
Cipher cipherEn = Cipher.getInstance("AES");
cipherEn.init(Cipher.ENCRYPT_MODE, seckey);
Cipher cipherDc = Cipher.getInstance("AES");
cipherDc.init(Cipher.DECRYPT_MODE, seckey);
cout = new CipherOutputStream(out, cipherEn);
cin = new CipherInputStream(in, cipherDc);
}
public InputStream getInputStream() throws IOException {
if (cin == null)
return super.getInputStream();
return cin;
}
public OutputStream getOutputStream() throws IOException {
if (cout == null)
return super.getOutputStream();
return cout;
}
public synchronized void close() throws IOException {
OutputStream o = getOutputStream();
o.flush();
super.close();
}
public void setkey(SecretKey seckey) throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException, IOException {
this.seckey = seckey;
Cipher cipherEn = Cipher.getInstance("AES");
cipherEn.init(Cipher.ENCRYPT_MODE, seckey);
cout = new CipherOutputStream(super.getOutputStream(), cipherEn);
Cipher cipherDc = Cipher.getInstance("AES");
cipherDc.init(Cipher.DECRYPT_MODE, seckey);
cin = new CipherInputStream(super.getInputStream(), cipherDc);
}
}
I can't figure out where is the problem. Thank you!
The problem is that a Cipher requires a specific amount of data (for example 128 bits) to be able to encrypt it correctly. If you send a file, that's no problem because the last few bits of the file will be sent when you close the stream.
However, you need a padding for network communication. You can specify one for your Cipher instances:
Cipher cipherEnOrDe = Cipher.getInstance("AES/CBC/PKCS5Padding"); //for example, check documentation for more
Using a padding, the Cipher will be able to send your data once you call the flush() method (which you should do whenever you want something to be sent anyway).
Note: Your application is only safe if your client is distributed with the public key. Otherwise, you cannot be sure that you are connecting to the right server in the first place. Anyone can create a public key.
Related
I want to be able to encrypt and decrypt strings over a simple client and server using an echo model architecture with a specified port.
The way the client and server communicate is as follows:
The client is run first and generates a key pair, prints out its public key to the console, and waits for an input (which will be server's public key via copy and paste)
The server is then run and does the same also. Printing out its public key and waits for the client's public key (also copy and paste.)
Copy and paste (client public key) is done to the server first, which then starts the server listening on port 4444.
The client is then also started with copy and paste (server public key), sending a string over port 4444.
encrypted with the server’s public key.
The strings are to be encrypted by the client, sent, and received by the server who will decrypt the strings. An additional feature I am thinking to add should allow multiple strings to be encrypted and decrypted (done by a while loop).
My console for the client seems to be able to send the strings (converted to bytes, then sent) just fine by using out.write() and out.flush(), but has trouble reading the encrypted bytes using (while data is not empty... in.read().
The exception thrown is Exception in thread "main" javax.crypto.BadPaddingException: Decryption error
My client code:
public class EchoClient {
private Socket clientSocket;
private DataOutputStream out;
private DataInputStream in;
private PrivateKey generateKeyPairClient() throws NoSuchAlgorithmException {
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
final KeyPair kp = kpg.generateKeyPair();
final PublicKey clientPublicKey = kp.getPublic();
System.out.println(clientPublicKey);
System.out.println("Client Public Key (Encoded) is " + Base64.getEncoder().encodeToString(clientPublicKey.getEncoded()));
return kp.getPrivate();
}
private PublicKey obtainServerPublicKey() {
System.out.println("Please enter the servers's public key below:");
Scanner sc = new Scanner(System.in);
String key = sc.next();
sc.close();
PublicKey serverPublicKey = null;
try{
byte[] byteKey = Base64.getDecoder().decode(key.getBytes());
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
serverPublicKey = kf.generatePublic(X509publicKey);
}
catch(Exception e){
System.out.println("The public key you entered for the server is not valid, please try again");
}
return serverPublicKey;
}
private void startConnection(String ip, int port) {
try {
clientSocket = new Socket(ip, port);
out = new DataOutputStream(clientSocket.getOutputStream());
in = new DataInputStream(clientSocket.getInputStream());
} catch (IOException e) {
System.out.println("Error when initializing connection");
}
}
private void sendPlainTextToEncryptedText(String original, PublicKey serverPublicKey, PrivateKey clientPrivateKey) {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, serverPublicKey);
byte[] originalBytes = original.getBytes(StandardCharsets.UTF_8);
byte[] cipherTextBytes = cipher.doFinal(originalBytes);
System.out.println("Client will send the ciphertext: "+ Util.bytesToHex(cipherTextBytes));
out.write(cipherTextBytes);
out.flush();
} catch (Exception e) {
System.out.println("There was a problem when trying to encrypt/decrypt, please try again");
e.printStackTrace();
return;
}
}
private void stopConnection() {
try {
in.close();
out.close();
clientSocket.close();
} catch (IOException e) {
System.out.println("error when closing");
}
}
public static void main(String[] args) throws NoSuchAlgorithmException {
EchoClient client = new EchoClient();
PrivateKey clientPrivateKey = client.generateKeyPairClient();
PublicKey serverPublicKey = client.obtainServerPublicKey();
System.out.println("Key Exchange Complete for Client");
client.startConnection("127.0.0.1", 4444);
client.sendPlainTextToEncryptedText("everyone", serverPublicKey, clientPrivateKey);
client.stopConnection();
}
}
My server code:
public class EchoServer {
private ServerSocket serverSocket;
private Socket clientSocket;
private DataOutputStream out;
private DataInputStream in;
private PrivateKey generateKeyPairServer() throws NoSuchAlgorithmException {
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
final KeyPair kp = kpg.generateKeyPair();
final PublicKey serverPublicKey = kp.getPublic();
System.out.println(serverPublicKey);
System.out.println("Server Public Key (Encoded) is " + Base64.getEncoder().encodeToString(serverPublicKey.getEncoded()));
return kp.getPrivate();
}
private PublicKey obtainClientPublicKey() {
System.out.println("Please enter the clients's public key below:");
Scanner sc = new Scanner(System.in);
String key = sc.next();
sc.close();
PublicKey clientPublicKey = null;
try{
byte[] byteKey = Base64.getDecoder().decode(key.getBytes());
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
clientPublicKey = kf.generatePublic(X509publicKey);
}
catch(Exception e){
System.out.println("The public key you entered for the client is not valid, please try again");
}
return clientPublicKey;
}
public void start(int port) {
try {
serverSocket = new ServerSocket(port);
clientSocket = serverSocket.accept();
out = new DataOutputStream(clientSocket.getOutputStream());
in = new DataInputStream(clientSocket.getInputStream());
} catch (IOException e) {
System.out.println("Error when establishing/accepting server connection");
}
}
private void receiveEncryptedTextToPlainText(PrivateKey serverPrivateKey) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
byte[] data = new byte[8];
#SuppressWarnings("unused")
int numBytes;
while ((numBytes = in.read(data)) != -1) {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, serverPrivateKey);
byte[] decryptedBytes = cipher.doFinal(data);
String text = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println("Server has recieved the plain text: "+ text);
}
stop();
}
public void stop() {
try {
in.close();
out.close();
clientSocket.close();
serverSocket.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, UnsupportedEncodingException, IOException, IllegalBlockSizeException, BadPaddingException {
EchoServer server = new EchoServer();
PrivateKey serverPrivateKey = server.generateKeyPairServer();
//Sorry for the mis-named var, will rename in code
PublicKey serverPublicKey = server.obtainClientPublicKey();
System.out.println("Key Exchange Complete for Server");
System.out.println("Now waiting for incoming connection...");
server.start(4444);
server.receiveEncryptedTextToPlainText(serverPrivateKey);
server.stop();
}
}
The Util assist class
import java.io.UnsupportedEncodingException;
public class Util {
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString();
}
public static String strToHex(String s) {
s = "failed decoding";
try {
s = Util.bytesToHex(s.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
System.out.println("Unsupported Encoding Exception");
}
return s;
}
}
The console outputs:
I'm not sure at to what might be wrong here, any advice?
I have noted two things which I think may be the cause:
Note: I am copying and pasting public keys (converting them to strings and back to public key objects - may be a problem...)
Note 2: The transfer of bytes over the socket in a size of 8 bytes might be problematic for longer strings
You must read all cipher-data before decrypt it. At EchoServer:
private void receiveEncryptedTextToPlainText(PrivateKey serverPrivateKey) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
byte[] data = new byte[8];
ByteArrayOutputStream cipherData = new ByteArrayOutputStream();
#SuppressWarnings("unused")
int numBytes;
while ((numBytes = in.read(data)) != -1) {
cipherData.write(data, 0, numBytes);
}
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, serverPrivateKey);
byte[] decryptedBytes = cipher.doFinal(cipherData.toByteArray());
String text = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println("Server has recieved the plain text: " + text);
stop();
}
I have a server which let's sockets connect to it to send data over inputstreams, this data is being encrypted with AES/GCM/NoPadding in a class called Cryptographer. The server has threads that hold functionalities for the connected clients, and each thread is being represented in a ConnectionThread class, this class holds a reference to the cryptograhper class which is being initialized in the server class.
Problem:
When I send my first command it works just fine, no problems at all. But somehow when I send my second command if gives the following stacktrace:
javax.crypto.AEADBadTagException: Tag mismatch!
at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:595)
at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116)
at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2208)
at com.company.security.Cryptographer.decrypt(Cryptographer.java:53)
at com.company.client.Reader.run(Reader.java:45)
at java.base/java.lang.Thread.run(Thread.java:835)
Exception in thread "Thread-3" java.lang.NullPointerException
at java.base/java.lang.String.<init>(String.java:623)
at com.company.client.Reader.run(Reader.java:47)
at java.base/java.lang.Thread.run(Thread.java:835)
These are the classes mentioned in the stacktrace
Cryptographer
package com.company.security;
import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class Cryptographer {
private Key secretKey;
private GCMParameterSpec gcmParameterSpec;
public Cryptographer() {
byte[] secret = new byte[16]; // 128 bit is 16 bytes, and AES accepts 16 bytes, and a few others.
byte[] secretBytes = "secret".getBytes();
byte[] IV = new byte[12];
gcmParameterSpec = new GCMParameterSpec(16 * 8, IV);
System.arraycopy(secretBytes, 0, secret, 0, secretBytes.length);
secretKey = new SecretKeySpec(secret, "AES");
}
/**
* Encrypt data.
* #param data to encrypt
* #return encrypted data
*/
public byte[] encrypt(byte[] data) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
byte[] encrypted = cipher.doFinal(data);
return encrypted;
} catch (InvalidKeyException | BadPaddingException
| IllegalBlockSizeException | NoSuchPaddingException
| NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
return null;
}
}
/**
* Decrypt data.
* #param data to decrypt
* #return decrypted data
*/
public byte[] decrypt(byte[] data) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
return cipher.doFinal(data);
} catch (InvalidKeyException | BadPaddingException
| IllegalBlockSizeException | NoSuchPaddingException
| NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
return null;
}
}
}
Reader
package com.company.client;
import com.company.FileLoader;
import com.company.client.helpers.ClientFileHelper;
import com.company.client.workers.MessageSender;
import com.company.security.Cryptographer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
public class Reader implements Runnable {
private InputStream inputStream;
private ClientFileHelper fileHelper;
private Cryptographer cryptographer;
private FileLoader fileLoader;
private BufferedReader bufferedReader;
private MessageSender messageSender;
private boolean isActive = true;
private boolean isReceivingFile = false;
public Reader(BufferedReader bufferedReader, MessageSender messageSender, InputStream inputStream) {
this.bufferedReader = bufferedReader;
this.messageSender = messageSender;
this.inputStream = inputStream;
cryptographer = new Cryptographer();
}
#Override
public void run() {
while (isActive) {
try {
int count;
byte[] buffer;
if(!isReceivingFile) {
buffer = new byte[inputStream.available()];
} else {
buffer = new byte[inputStream.available()];
}
while ((count = inputStream.read(buffer)) > 0)
{
byte[] decrypted = cryptographer.decrypt(buffer);
if(!isReceivingFile) {
handleInput(new String(decrypted));
} else {
if(fileHelper.getFileBytes().length == 0) {
fileHelper.setFileBytes(decrypted);
} else {
fileHelper.saveFile();
isReceivingFile = false;
}
}
}
} catch (IOException e) {
e.printStackTrace();
break;
}
}
}
/**
* Handle the user input form the console.
* #param input user input from console
*/
private void handleInput(String input) {
try {
if (input.equals("PING")) { // If we get a PING message we send back a PONG message.
messageSender.send("PONG");
} else if (input.contains("FILE")) {
setupFileAccept(input);
isReceivingFile = true;
} else {
System.out.println(input);
}
} catch (Exception ex) {
isActive = false;
}
}
/**
* Setup the file helper for the client that's going to receive a file.
* #param line command
*/
private void setupFileAccept(String line) {
String[] args = line.split(" ");
if(args[0].equals("FILE")) {
fileHelper = new ClientFileHelper(args[1], Integer.valueOf(args[2]));
}
}
}
The ConnectionThread also has a similar read functionality which looks like this:
while (isActive) {
try {
int count;
byte[] buffer;
if(!isReceivingFile) {
buffer = new byte[inputStream.available()];
} else {
buffer = fileHelper.getFileBytes();
}
while ((count = inputStream.read(buffer)) > 0)
{
byte[] decrypted = server.cryptographer.decrypt(buffer);
if(!isReceivingFile) {
getInput(new String(decrypted));
} else {
fileHelper.setFileBytes(decrypted);
// bytes received, now we can send the file!
if(fileHelper.sendToReceiver()) {
writeToClient(fileHelper.getReceiverName()
+ " received " + fileHelper.getFilename());
fileHelper = null;
}
}
}
} catch (IOException e) {
e.printStackTrace();
break;
}
}
In this case just assume that the Server class has the cryptographer property properly initialized, which is always the case.
My guess is that somewhere a value is doing something wrong but I am not sure. I am rather clueless to what I should do to fix this problem. Can somebody help me point out the mistakes and come up with possible solutions to fix this problem? My java version is 12.0.1
I would encourage to cosider to use SSL/TLS or DTLS instead of trying to reimplement parts of it.
Whether it causes your error I'm not sure but if my interpretation of the Java documentation is correct than you should change the GCMParameterSpec for each message:
after each encryption operation using GCM mode, callers should
re-initialize the cipher objects with GCM parameters which has a
different IV value
and:
GCM mode has a uniqueness requirement on IVs used in encryption with a given key. When IVs are repeated for GCM encryption, such usages are subject to forgery attacks.
Also you are not using the updateAAD (Additional Authentication Data), although that is optional according to https://www.rfc-editor.org/rfc/rfc5084 from the error message it sounds like it is causing errors here, but it may just be a misleading error message.
Update:
I wrote lots of unit tests for the Cryptographer class and only if I start to make random changes to the ciphertext before I decrypt it again I often get the same error. Because we can trust TCP/IP to reproduce exactly the same bytes on the other side of the connection we're left with these kind of problems may be:
Concurrency
Converting ciphertext bytes into Strings, Chars, Readers/Writers
Not reading the entire message from the socket (did you check how many bytes you sent and compared it to how many you received?
And no, I have not yet written and tested my own implementation yet, but there are examples out there, like this example, nicely explained by the author here from the code was found by this search
THanks to JohannesB for pointing me into the right direction!
I now have solved my problems. It first started by the reading method which I had to change to this:
byte[] buffer;
while (inputStream.available() > 0)
{
int read = inputStream.read(buffer);
if(read == 0)
break;
}
// An if statement checking if the buffer has been filled and based on this
// It will execute methods
And my Cryptographer class now looks like this:
public class Cryptographer {
private SecretKey secretKey;
private byte[] aad;
private SecureRandom secureRandom;
private byte[] IV;
public Cryptographer(SecretKey secretKey) {
this.secretKey = secretKey;
secureRandom = new SecureRandom();
IV = new byte[12];
secureRandom.nextBytes(IV);
aad = "association".getBytes();
}
/**
* Encrypt data.
* #param data to encrypt
* #return encrypted data
*/
public byte[] encrypt(byte[] data) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
secureRandom.nextBytes(IV);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
cipher.updateAAD(aad);
return toByteBuffer(cipher.doFinal(data));
} catch (InvalidKeyException | BadPaddingException
| IllegalBlockSizeException | NoSuchPaddingException
| NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
return null;
}
}
/**
* Decrypt data.
* #param data to decrypt
* #return decrypted data
*/
public byte[] decrypt(byte[] data) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
// get the data from the byte buffer
data = fromByteBuffer(data);
// create the gcm parameter with the received IV.
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
cipher.updateAAD(aad);
return cipher.doFinal(data);
} catch (InvalidKeyException | BadPaddingException
| IllegalBlockSizeException | NoSuchPaddingException
| NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
return null;
}
}
/**
* Put the encrypted data through a byte buffer.
* This buffer will contain information about the IV array.
* #param data encrypted data
* #return the ByteBuffer result as byte array
*/
private byte[] toByteBuffer(byte[] data) {
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + IV.length + data.length);
byteBuffer.putInt(IV.length);
byteBuffer.put(IV);
byteBuffer.put(data);
return byteBuffer.array();
}
/**
* Gets data from a ByteBuffer and sets up data needed for decryption.
* #param data ByteBuffer data as byte array
* #return ByteBuffer encrypted data
*/
private byte[] fromByteBuffer(byte[] data) {
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
int ivLength = byteBuffer.getInt();
if(ivLength < 12 || ivLength >= 16) {
throw new IllegalArgumentException("invalid iv length");
}
IV = new byte[ivLength];
byteBuffer.get(IV);
byte[] remaining = new byte[byteBuffer.remaining()];
byteBuffer.get(remaining);
return remaining;
}
}
As for the reasons why I did this you can see JohannesB's suggestions and check out these articles:
https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9
How to read all of Inputstream in Server Socket JAVA
I have an RSA encryption system for securing the communication between a Server (Python) and a Client (Java). Currently, I manage to encrypt and decrypt correctly information from the client to the server using the server's keys. What I am not able to do, is to decrypt the message on the Client (Java) that comes from the Server (Python).
The encryption snippet in Python is the following:
publicKeyStr = None
with open(PATH_KEY, 'r') as file:
publicKeyStr = file.read()
publicKeyClient = RSA.importKey(publicKeyStr)
encryptor = PKCS1_v1_5.new(key)
message = encryptor.encrypt(plainMessage)
# Message's length is always 128 bytes on the server side
#Send message to the client (RabbitMQ)
The variable message with the ciphered content is received on the Client side in String format and here comes the problem: the decryption method in Java expects a byte[] input and a maximum block size of 128 bytes, but when transforming the String message to a byte[] array, these String has over 128 bytes. This was my approach on the client side (Java):
public String call(String queue, byte[] message) throws IOException,
InterruptedException {
final String corrId = UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.correlationId(corrId).replyTo(replyQueueName).build();
channel.basicPublish("", queue, props, message);
final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);
String ctag = channel.basicConsume(replyQueueName, true,
new DefaultConsumer(channel) {
#Override
public void handleDelivery(String consumerTag,
Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
if (properties.getCorrelationId().equals(corrId)) {
response.offer(new String(body, "UTF-8"));
}
}
});
String result = response.take();
channel.basicCancel(ctag);
return result;
}
public static String sendElement(String topic, byte[] message) {
SendRabbitMQ sendRabbitMQ = null;
String response = "";
try {
sendRabbitMQ = new SendRabbitMQ();
response = sendRabbitMQ.call(topic, message);
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
} finally {
if (sendRabbitMQ != null) {
try {
sendRabbitMQ.close();
} catch (IOException _ignore) {
}
}
}
return response;
}
String response = SendRabbitMQ.sendElement("topic", encryptedInfo);
byte [] responseByte = response.getBytes();
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
System.out.println(new String(cipher.doFinal(message)));
The error raises when responseByte is assigned, as it's size is over 128 characters.
Therefore, my question is, how can I transform the message String sent from the server side on the client size in order to correctly transform this String to a byte[] array?
EDIT: I believe it does not have to do with the RabbitMQ code on the client side, but just in case, I have included the methods used.
With the code below I can easily encrypt a file and decrypt it, using the password that the user enters. I am now imploding the use of the fingerprint to enter my app. When the fingerprint is correct, I want to decrypt the file. But how do I do if the user has not entered the password? is there a relatively safe way to do this? Thanks
public class CryptoUtils {
private static final String ALGORITHM = "AES";
private static final String ENCRYPTION_IV = "4e5Wa71fYoT7MFEX";
private static String ENCRYPTION_KEY = "";
public static void encrypt(String key, File inputFile, File outputFile)
throws CryptoException {
doCrypto(Cipher.ENCRYPT_MODE, key, inputFile, outputFile);
}
public static void decrypt(String key, File inputFile, File outputFile)
throws CryptoException {
doCrypto(Cipher.DECRYPT_MODE, key, inputFile, outputFile);
}
private static void doCrypto(int cipherMode, String key, File inputFile, File outputFile)
throws CryptoException {
try {
ENCRYPTION_KEY = key;
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(cipherMode, makeKey(), makeIv());
FileInputStream inputStream = new FileInputStream(inputFile);
byte[] inputBytes = new byte[(int) inputFile.length()];
inputStream.read(inputBytes);
byte[] outputBytes = cipher.doFinal(inputBytes);
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(outputBytes);
inputStream.close();
outputStream.close();
//Log.d("cur__", "Encryption/Decryption Completed Succesfully");
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException | IOException ex) {
throw new CryptoException("Error encrypting/decrypting file " + ex.getMessage(), ex);
} catch (InvalidAlgorithmParameterException ex) {
Logger.getLogger(CryptoUtils.class.getName()).log(Level.SEVERE, null, ex);
}
}
private static AlgorithmParameterSpec makeIv() {
return new IvParameterSpec(ENCRYPTION_IV.getBytes(StandardCharsets.UTF_8));
}
private static Key makeKey() {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] key = md.digest(ENCRYPTION_KEY.getBytes(StandardCharsets.UTF_8));
//Log.d("Lenghtis", new SecretKeySpec(key, ALGORITHM).getEncoded().length + "");
return new SecretKeySpec(key, ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
You first need to generate a Key. You can use the following function
fun generateKey(storeKey: StoreKey): SecretKey {
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "MyKeyStore")
val keyGenParameterSpec = KeyGenParameterSpec.Builder(storeKey.Alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setRandomizedEncryptionRequired(false)
.build()
keyGenerator.init(keyGenParameterSpec)
return keyGenerator.generateKey()
}
You need setUserAuthenticationRequired(true) for biometric authentication.
Then you can provide this key to your biometric authenticator to authenticate the user and in return you will receive cipher from it.
Then you can use that cipher (encrypt cipher OR decrypt cipher) to perform encrypt/decrypt data directly using this cipher.
androidx.biometric:biometric:1.0.0-beta01
like this
val biometricCallbacks = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
result.cryptoObject?.cipher?.let { cipher ->
val encryptedData = cipherUtils.encryptAES(dataToEncrypt, cipher)
dismiss()
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
if (errorCode == BiometricPrompt.ERROR_CANCELED || errorCode == BiometricPrompt.ERROR_USER_CANCELED) {
dismiss()
}
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.e("BIOMETRIC", "FAILED")
}
}
val biometricPrompt = BiometricPrompt(this, biometricExecutor, biometricCallbacks)
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("APP")
.setSubtitle("Enable Biometric for Encryption")
.setDescription("")
.setNegativeButtonText("Cancel")
.build()
biometricPrompt.authenticate(
biometricPromptInfo,
BiometricPrompt.CryptoObject(
cipherUtils.getBioEncryptCipher(keyStoreUtils.generateNewKey(StoreKey.Pass, true))
)
)
You can encrypt users password and keep it securely in database somewhere and use the biometric authentication. Since the key you are generating is strictly bind to users biometrics, users encrypted password will not get compromised.
Here's the androidx.biometric demo app for using biometric-bound keys (key can only be unlocked by strong biometric sensors).
Basically
Generate a key that's only usable after being authenticated via biometrics, with the associated capabilities you'd like the key to be able to perform (encrypt, decrypt, etc)
Wrap the cryptographic operation you wish to perform in a CryptoObject. The key will only become usable after a user has authenticated with biometrics
Invoke BiometricPrompt#authenticate(CryptoObject)
Use the key after onAuthenticationSucceeded
For completeness, there's also authentication-bound keys (key can be unlocked by biometrics OR device credential), which become available for use by your app whenever the user has authenticated within t seconds, where t is specified in KeyGenParameterSpec.Builder#setUserAuthenticationValidityDurationSeconds. You can see an example usage here.
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:
Client generates RSA public/private key pair
Client sends out his RSA public key to the server
Server generates and encrypts AES key with client's RSA public key
Server sends encrypted AES key to client
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();
}
}
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);