Ident protocol client supporting authd OpenSSL encrypted usernames
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 26 Jul 2019 21:15:15 +0000 (23:15 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 26 Jul 2019 21:15:15 +0000 (23:15 +0200)
org.argeo.enterprise/src/org/argeo/ident/IdentClient.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java [new file with mode: 0644]

diff --git a/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java b/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java
new file mode 100644 (file)
index 0000000..32fb28b
--- /dev/null
@@ -0,0 +1,65 @@
+package org.argeo.ident;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.StringTokenizer;
+
+/**
+ * A simple ident client, supporting authd OpenSSL encrypted username.
+ * 
+ * @see RFC 1413 https://tools.ietf.org/html/rfc1413
+ */
+public class IdentClient {
+       private String host = "localhost";
+       private int port = 113;
+
+       private OpenSslDecryptor openSslDecryptor = new OpenSslDecryptor();
+       private String identPassphrase = "changeit";
+
+       public IdentClient(String host, String identPassphrase) {
+               this(host, identPassphrase, 113);
+       }
+
+       public IdentClient(String host, String identPassphrase, int port) {
+               this.host = host;
+               this.identPassphrase = identPassphrase;
+               this.port = port;
+       }
+
+       public String getUsername(int serverPort, int clientPort) {
+               String answer;
+               try (Socket socket = new Socket(host, port)) {
+                       String msg = serverPort + "," + clientPort + "\n";
+                       OutputStream out = socket.getOutputStream();
+                       out.write(msg.getBytes(StandardCharsets.US_ASCII));
+                       out.flush();
+                       BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+                       answer = reader.readLine();
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot read from ident server on " + host + ":" + port, e);
+               }
+               StringTokenizer st = new StringTokenizer(answer, " :\n");
+               String username = null;
+               while (st.hasMoreTokens())
+                       username = st.nextToken();
+               if (username.startsWith("[")) {
+                       String encrypted = username.substring(1, username.length() - 1);
+                       username = openSslDecryptor.decryptAuthd(encrypted, identPassphrase).trim();
+               }
+//             System.out.println(username);
+               return username;
+       }
+
+       public void setOpenSslDecryptor(OpenSslDecryptor openSslDecryptor) {
+               this.openSslDecryptor = openSslDecryptor;
+       }
+
+       public static void main(String[] args) {
+               IdentClient identClient = new IdentClient("127.0.0.1", "changeit");
+               String username = identClient.getUsername(7070, 55958);
+               System.out.println(username);
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java b/org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java
new file mode 100644 (file)
index 0000000..702b09b
--- /dev/null
@@ -0,0 +1,162 @@
+package org.argeo.ident;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Base64;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Decrypts OpenSSL encrypted data.
+ * 
+ * From
+ * https://stackoverflow.com/questions/11783062/how-to-decrypt-file-in-java-encrypted-with-openssl-command-using-aes
+ * 
+ * See also
+ * https://stackoverflow.com/questions/54171959/badpadding-exception-when-trying-to-decrypt-aes-based-encrypted-text/54173509#54173509
+ * for new default message digest (not yet in CentOS 7 as of July 2019)
+ */
+public class OpenSslDecryptor {
+       private static final int INDEX_KEY = 0;
+       private static final int INDEX_IV = 1;
+       private static final int ITERATIONS = 1;
+
+       private static final int SALT_OFFSET = 8;
+       private static final int SALT_SIZE = 8;
+       private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE;
+
+       /** In bits. */
+       private final int keySize;
+
+       private Cipher cipher;
+       private MessageDigest messageDigest;
+
+       public OpenSslDecryptor() {
+               /*
+                * Changed to SHA-256 from OpenSSL v1.1.0 (see
+                * https://stackoverflow.com/questions/39637388/encryption-decryption-doesnt-
+                * work-well-between-two-different-openssl-versions)
+                */
+               this(128, "MD5");
+       }
+
+       public OpenSslDecryptor(int keySize, String messageDigest) {
+               this.keySize = keySize;
+               try {
+                       this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+                       this.messageDigest = MessageDigest.getInstance(messageDigest);
+               } catch (GeneralSecurityException e) {
+                       throw new IllegalArgumentException("Cannot initialise decryptor", e);
+               }
+       }
+
+       public String decryptAuthd(String dataBase64, String passphrase) {
+               try {
+                       byte[] headerSaltAndCipherText = Base64.getDecoder().decode(dataBase64);
+
+                       boolean withSalt = true;
+                       byte[] salt = withSalt ? Arrays.copyOfRange(headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE)
+                                       : null;
+                       byte[] encrypted = withSalt
+                                       ? Arrays.copyOfRange(headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length)
+                                       : headerSaltAndCipherText;
+
+                       final byte[][] keyAndIV = EVP_BytesToKey(keySize / Byte.SIZE, cipher.getBlockSize(), messageDigest, salt,
+                                       passphrase.getBytes(StandardCharsets.US_ASCII), ITERATIONS);
+                       SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES");
+                       IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]);
+
+                       cipher.init(Cipher.DECRYPT_MODE, key, iv);
+                       byte[] decrypted = cipher.doFinal(encrypted);
+
+                       String answer = new String(decrypted, StandardCharsets.US_ASCII);
+                       return answer;
+               } catch (BadPaddingException e) {
+                       throw new IllegalStateException("Bad password, algorithm, mode or padding;"
+                                       + " no salt, wrong number of iterations or corrupted ciphertext.", e);
+               } catch (IllegalBlockSizeException e) {
+                       throw new IllegalStateException("Bad algorithm, mode or corrupted (resized) ciphertext.", e);
+               } catch (GeneralSecurityException e) {
+                       throw new IllegalStateException(e);
+               }
+       }
+
+       private static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data,
+                       int count) {
+               byte[][] both = new byte[2][];
+               byte[] key = new byte[key_len];
+               int key_ix = 0;
+               byte[] iv = new byte[iv_len];
+               int iv_ix = 0;
+               both[0] = key;
+               both[1] = iv;
+               byte[] md_buf = null;
+               int nkey = key_len;
+               int niv = iv_len;
+               int i = 0;
+               if (data == null) {
+                       return both;
+               }
+               int addmd = 0;
+               for (;;) {
+                       md.reset();
+                       if (addmd++ > 0) {
+                               md.update(md_buf);
+                       }
+                       md.update(data);
+                       if (null != salt) {
+                               md.update(salt, 0, 8);
+                       }
+                       md_buf = md.digest();
+                       for (i = 1; i < count; i++) {
+                               md.reset();
+                               md.update(md_buf);
+                               md_buf = md.digest();
+                       }
+                       i = 0;
+                       if (nkey > 0) {
+                               for (;;) {
+                                       if (nkey == 0)
+                                               break;
+                                       if (i == md_buf.length)
+                                               break;
+                                       key[key_ix++] = md_buf[i];
+                                       nkey--;
+                                       i++;
+                               }
+                       }
+                       if (niv > 0 && i != md_buf.length) {
+                               for (;;) {
+                                       if (niv == 0)
+                                               break;
+                                       if (i == md_buf.length)
+                                               break;
+                                       iv[iv_ix++] = md_buf[i];
+                                       niv--;
+                                       i++;
+                               }
+                       }
+                       if (nkey == 0 && niv == 0) {
+                               break;
+                       }
+               }
+               for (i = 0; i < md_buf.length; i++) {
+                       md_buf[i] = 0;
+               }
+               return both;
+       }
+
+       public static void main(String[] args) {
+               String dataBase64 = args[0];
+               String passphrase = args[1];
+               OpenSslDecryptor decryptor = new OpenSslDecryptor();
+               System.out.println(decryptor.decryptAuthd(dataBase64, passphrase));
+       }
+
+}
\ No newline at end of file