Java: 将使用服务器 public 密钥加密的随机对称密钥发送到此服务器是否安全?

Java: Is it secure to send a random symmetric key encrypted with servers public key to this server?

我不确定这个问题是属于 Whosebug 还是 crypto stackexchange,但因为它包含源代码,所以我将 post 放在这里。

这是我的问题: 我写了两个程序,一个是客户端,一个是服务端。他们通过使用 AES 加密来安全通信。客户端生成随机对称密钥,用服务器的public密钥加密后发送给服务器。然后服务器可以解密密钥并使用它与客户端通信。

我听说过 diffie hellman 密钥交换,想知道我的代码是否和它一样安全。按我的方式做有风险吗,使用 diffie hellman 密钥交换有什么优势吗?

客户来源:

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;

public class Client {
    public static HashMap<String, String> arguments = new HashMap<>();
    public static String sessionKey;
    public static void start(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InterruptedException, TimeoutException {
        if(args.length % 2 != 0) {
            System.out.println("Ungültige Argumentelänge: Muss gerade sein, Muster: Feld Wert");
            return;
        }
        for(int i = 0; i < args.length; i+=2) {
            switch (args[i].toLowerCase()) {
                case "help":
                    System.out.println("");
                    break;
                case "ip":
                    arguments.put("ip", args[i + 1].toLowerCase());
                    break;
                case "publickey":
                    arguments.put("publickey", args[i + 1]);
                    break;
                default:
                    System.out.println("Unbekannte Option: " + args[i]);
                    break;
            }
        }
        if(arguments.containsKey("ip") && arguments.containsKey("publickey")) {
            Socket s = new Socket();
            s.connect(new InetSocketAddress(arguments.get("ip"), 6577));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            PublicKey publicKey = Main.publicKeyFromString(String.join("", Main.read(new File(arguments.get("publickey")))));
            sessionKey = Main.generateSessionKey();
            String encryptedSessionKey = Main.encrypt(sessionKey, publicKey, Main.RSA);
            bw.write(encryptedSessionKey);
            bw.flush();
            SecretKeySpec key = Main.StringtoKey(sessionKey);
            BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            BufferedReader inbr = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                if(inbr.ready()) {
                    String line = Main.readAsMuchAsPossible(System.in);
                    bw.write(Main.encrypt(line, key, "AES") + "\n");
                    bw.flush();
                }
                if(br.ready()) {
                    String line2 = Main.readAsMuchAsPossible(s.getInputStream());
                    System.out.println(Main.decrypt(line2, key, "AES"));
                }
            }
            //System.out.println(Main.decrypt(read, , "AES"));
        }
    }
}

服务器来源:

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;

public class Server {
    public static HashMap<String, String> arguments = new HashMap<>();
    static PrivateKey privateKey;
    public static void start(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InterruptedException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException, TimeoutException {
        if(args.length % 2 != 0) {
            System.out.println("Ungültige Argumentelänge: Muss gerade sein, Muster: Feld Wert");
            return;
        }
        for(int i = 0; i < args.length; i+=2) {
            if (args[i].toLowerCase().equals("privkey")) {
                arguments.put("privkey", args[i + 1]);
            } else {
                System.out.println("Unbekannte Option: " + args[i]);
            }
        }
        if(arguments.containsKey("privkey")) {
            String encodedPrivateKey = String.join("", Main.read(new File(arguments.get("privkey"))));
            privateKey = Main.privateKeyFromString(encodedPrivateKey);
            ServerSocket serverSocket = new ServerSocket(6577);
            while (true) {
                final Socket socket = serverSocket.accept();
                /*Thread t = new Thread(() -> {
                    try {
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
                t.start();*/
                System.out.println("Verbindung empfangen!");
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                int timeout = 100;
                int current = 0;
                while (!br.ready()) {
                    Thread.sleep(100);
                    if (current >= timeout) {
                        System.out.println("Timeout erreicht, Client reagiert nicht, Verbindung wird geschlossen!");
                        socket.close();
                        return;
                    }
                    current++;
                }
                String read = Main.readFromInputStream(socket.getInputStream(), 10000, 100);
                if (read.equals("")) {
                    System.out.println("read ist leer");
                    return;
                }
                String sessionkey = Main.decrypt(read, privateKey, Main.RSA);
                SecretKeySpec key = Main.StringtoKey(sessionkey);
                BufferedReader inbr = new BufferedReader(new InputStreamReader(System.in));
                while (true) {
                    if (inbr.ready()) {
                        String line2 = Main.readAsMuchAsPossible(System.in);
                        bw.write(Main.encrypt(line2, key, "AES") + "\n");
                        bw.flush();
                    }
                    if (br.ready()) {
                        String line2 = br.readLine();//Main.readAsMuchAsPossible(socket.getInputStream());
                        String decrypted = Main.decrypt(line2, key, "AES");
                        if(decrypted.startsWith("cmd")) {
                            String[] arr = decrypted.split(" ");
                            String cmd = String.join(" ", Arrays.copyOfRange(arr, 1, arr.length));
                            System.out.println("cmd: " + cmd);
                            Process process = Runtime.getRuntime().exec(cmd);
                            InputStream in = process.getInputStream();
                            do {
                                StringBuilder complete = new StringBuilder();
                                while (in.available() > 0) {
                                    complete.append((char)in.read());
                                }
                                if(!complete.toString().equals(""))  {
                                    bw.write(Main.encrypt("processout: \"" + complete.toString() + "\"", key, "AES"));
                                    bw.flush();
                                }
                            } while (process.isAlive());
                        }
                        System.out.println(decrypted);
                    }
                }
            }
        } else {
            System.out.println("Kein privater Schlüssel angegeben!");
        }
    }
}

我有一个实用程序 class,这就是 "Main" 所指的:

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;
import java.util.concurrent.TimeoutException;

public class Main {
    public static final String RSA = "RSA";

    public static String[] read(File f) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(f));
        ArrayList<String> lines = new ArrayList<>();
        String line;
        while((line = br.readLine()) != null) {
            lines.add(line);
        }
        String[] array = new String[lines.size()];
        lines.toArray(array);
        return array;
    }

    public static PrivateKey privateKeyFromString(String s) throws InvalidKeySpecException, NoSuchAlgorithmException {
        return privateKeyFromBytes(Base64.getDecoder().decode(s));
    }

    public static PrivateKey privateKeyFromBytes(byte[] bytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
        PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(bytes);
        KeyFactory kf = KeyFactory.getInstance(RSA);
        return kf.generatePrivate(ks);
    }

    public static String privateKeyToString(PrivateKey k) {
        return Base64.getEncoder().encodeToString(privateKeyToBytes(k));
    }

    public static byte[] privateKeyToBytes(PrivateKey k) {
        PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(k.getEncoded());
        return ks.getEncoded();
    }

    public static PublicKey publicKeyFromString(String s) throws NoSuchAlgorithmException, InvalidKeySpecException {
        return publicKeyFromBytes(Base64.getDecoder().decode(s));
    }

    public static PublicKey publicKeyFromBytes(byte[] bytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
        X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
        KeyFactory kf = KeyFactory.getInstance(RSA);
        return kf.generatePublic(ks);
    }

    public static String publicKeyToString(PublicKey k) {
        return Base64.getEncoder().encodeToString(publicKeyToBytes(k));
    }

    public static byte[] publicKeyToBytes(PublicKey k) {
        X509EncodedKeySpec ks = new X509EncodedKeySpec(k.getEncoded());
        return ks.getEncoded();
    }

    public static String readFromInputStream(InputStream i, int timeoutmillis, int period) throws IOException, InterruptedException, TimeoutException {
        int current = 0;
        while (i.available() == 0) {
            Thread.sleep(period);
            current = current + period;
            if(current > timeoutmillis) {
                throw new TimeoutException("Timeout erreicht");
            }
        }
        ArrayList<Character> buffer = new ArrayList<>();
        while(i.available() > 0) {
            int c = i.read();
            if(c == -1) {
                return BuffertoString(buffer);
            } else {
                buffer.add((char) c);
            }
        }
        return BuffertoString(buffer);
    }
    public static String BuffertoString(ArrayList<Character> buffer) {
        StringBuilder out = new StringBuilder();
        for(char a : buffer) {
            out.append(a);
        }
        return out.toString();
    }

    public static String encrypt(String msg, Key key, String verfahren) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        return Base64.getEncoder().encodeToString(encryptBytes(msg.getBytes(), key, verfahren));
    }

    public static byte[] encryptBytes(byte[] msg, Key key, String verfahren) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
        Cipher c = Cipher.getInstance(verfahren);
        c.init(Cipher.ENCRYPT_MODE, key);
        return c.doFinal(msg);
    }

    public static String decrypt(String msg, Key key, String verfahren) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        return new String(decryptBytes(Base64.getDecoder().decode(msg), key, verfahren));
    }

    public static byte[] decryptBytes(byte[] msg, Key key, String verfahren) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
        Cipher c = Cipher.getInstance(verfahren);
        c.init(Cipher.DECRYPT_MODE, key);
        return c.doFinal(msg);
    }

    static final String lowercase = "abcdefghijklmnopqrstuvwxyz";
    static final String uppercase = lowercase.toUpperCase();
    static final String digits = "0123456789";
    static final char[] combined = (uppercase + lowercase + digits).toCharArray();

    public static String generateSessionKey() {
        StringBuilder resultBuilder = new StringBuilder();
        Random random = new Random();
        for(int i = 0; i < 128; i++) {
            resultBuilder.append(combined[(int)(random.nextDouble() * combined.length)]);
        }
        return resultBuilder.toString();
    }

    public static SecretKeySpec StringtoKey(String s) throws NoSuchAlgorithmException {
        byte[] digest = MessageDigest.getInstance("SHA-256").digest(s.getBytes());
        digest = Arrays.copyOf(digest, 16);
        return new SecretKeySpec(digest, "AES");
    }
    public static String readAsMuchAsPossible(InputStream in) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        while(in.available() > 0) {
            int c = in.read();
            if(c == -1) {
                return stringBuilder.toString();
            } else {
                stringBuilder.append((char) c);
            }
        }
        return stringBuilder.toString();
    }
}

另外,请注意代码非常简单,还没有处理异常。目前只是一个原型。

They communicate securely by encrypting with AES.

我的理解是:我正在尝试实施传输协议,但我不知道正确使用 AES 的不同操作模式。

The client generates a random symmetric key, encrypts it with the public key of the server and sends it to the server. The server can then decrypt the secret key and use it to communicate with the client.

是的,这就是混合加密(非对称和对称加密)的工作原理。

I heard about the diffie hellman key exchange, and wondered if my code is as secure as that.

您似乎使用了证书,这表明您可能已经考虑过确保 public 密钥可以信任。是的,TLS 中的所有 RSA 密码套件(包括 1.2)都使用主密钥的 RSA 加密,然后从中导出会话密钥 .

Is it risky to do it my way, and is there any kind of advantage using the Diffie-Hellman key exchange?

是的。如果您使用短暂的 Diffie-Hellman,则可能具有前向安全性。这意味着即使静态密钥丢失,也无法解密会话。您当然仍然需要单独的静态 (RSA) 密钥来验证服务器和可能的客户端。这是 TLS 1.3 不再具有 RSA_ 密码套件的原因之一。

RSA 无法真正实现前向安全,因为 RSA 密钥对生成效率太低。


可以这么说,创建加密安全传输协议并不适合外行。即使没有看到所有代码,我也可以看出您的协议在很多方面都不安全;请改用 TLS。