From 1d8a16106116f22b91ecfe71fb05b45d050834b9 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 26 Jul 2019 23:15:15 +0200 Subject: [PATCH] Ident protocol client supporting authd OpenSSL encrypted usernames --- .../src/org/argeo/ident/IdentClient.java | 65 +++++++ .../src/org/argeo/ident/OpenSslDecryptor.java | 162 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 org.argeo.enterprise/src/org/argeo/ident/IdentClient.java create mode 100644 org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java 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 index 000000000..32fb28ba1 --- /dev/null +++ b/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java @@ -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 index 000000000..702b09bfe --- /dev/null +++ b/org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java @@ -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 -- 2.30.2