Java tutorial
/* * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org> * * This file is part of the Kitodo project. * * It is licensed under GNU General Public License version 3 or later. * * For the full copyright and license information, please read the * GPL3-License.txt file that was distributed with this source code. */ package org.kitodo.production.services.data; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Objects; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.ModificationItem; import javax.naming.directory.SearchResult; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import org.apache.commons.codec.binary.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bouncycastle.jce.provider.JDKMessageDigest; import org.kitodo.config.ConfigCore; import org.kitodo.config.enums.ParameterCore; import org.kitodo.data.database.beans.LdapServer; import org.kitodo.data.database.beans.User; import org.kitodo.data.database.enums.PasswordEncryption; import org.kitodo.data.database.exceptions.DAOException; import org.kitodo.data.database.persistence.LdapServerDAO; import org.kitodo.production.helper.Helper; import org.kitodo.production.ldap.LdapUser; import org.kitodo.production.security.password.SecurityPasswordEncoder; import org.kitodo.production.services.ServiceManager; import org.kitodo.production.services.data.base.SearchDatabaseService; import org.primefaces.model.SortOrder; public class LdapServerService extends SearchDatabaseService<LdapServer, LdapServerDAO> { private static final Logger logger = LogManager.getLogger(LdapServerService.class); private static LdapServerService instance = null; private SecurityPasswordEncoder passwordEncoder = new SecurityPasswordEncoder(); /** * Return singleton variable of type LdapServerService. * * @return unique instance of LdapServerService */ public static LdapServerService getInstance() { if (Objects.equals(instance, null)) { synchronized (LdapServerService.class) { if (Objects.equals(instance, null)) { instance = new LdapServerService(); } } } return instance; } private LdapServerService() { super(new LdapServerDAO()); } @Override public Long countDatabaseRows() throws DAOException { return countDatabaseRows("SELECT COUNT(*) FROM LdapServer"); } @Override public Long countResults(Map filters) throws DAOException { return countDatabaseRows(); } @Override public List<LdapServer> getAllForSelectedClient() { throw new UnsupportedOperationException(); } @Override public List<LdapServer> loadData(int first, int pageSize, String sortField, SortOrder sortOrder, Map filters) { return new ArrayList<>(); } private String buildUserDN(User inUser) { String userDN = inUser.getLdapGroup().getUserDN(); userDN = userDN.replaceAll("\\{login\\}", inUser.getLogin()); if (Objects.nonNull(inUser.getLdapLogin())) { userDN = userDN.replaceAll("\\{ldaplogin\\}", inUser.getLdapLogin()); } userDN = userDN.replaceAll("\\{firstname\\}", inUser.getName()); userDN = userDN.replaceAll("\\{lastname\\}", inUser.getSurname()); return userDN; } private Hashtable<String, String> initializeWithLdapConnectionSettings(LdapServer ldapServer) { Hashtable<String, String> env = new Hashtable<>(11); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, ldapServer.getUrl()); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, ldapServer.getManagerLogin()); String encryptedManagerPassword = ldapServer.getManagerPassword(); String decryptedManagerPassword = passwordEncoder.decrypt(encryptedManagerPassword); env.put(Context.SECURITY_CREDENTIALS, decryptedManagerPassword); if (ldapServer.isUseSsl()) { String keystorepath = ldapServer.getKeystore(); String keystorepasswd = ldapServer.getKeystorePassword(); // add all necessary certificates first loadCertificates(keystorepath, keystorepasswd, ldapServer); // set properties, so that the current keystore is used for SSL System.setProperty("javax.net.ssl.keyStore", keystorepath); System.setProperty("javax.net.ssl.trustStore", keystorepath); System.setProperty("javax.net.ssl.keyStorePassword", keystorepasswd); env.put(Context.SECURITY_PROTOCOL, "ssl"); } return env; } /** * create new user in LDAP-directory. * * @param user * User object * @param password * String */ public void createNewUser(User user, String password) throws NamingException, NoSuchAlgorithmException, IOException { if (!user.getLdapGroup().getLdapServer().isReadOnly()) { Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings( user.getLdapGroup().getLdapServer()); LdapUser ldapUser = new LdapUser(); ldapUser.configure(user, password, getNextUidNumber(user.getLdapGroup().getLdapServer())); DirContext ctx = new InitialDirContext(ldapEnvironment); ctx.bind(buildUserDN(user), ldapUser); ctx.close(); setNextUidNumber(user.getLdapGroup().getLdapServer()); Helper.setMessage( Helper.getTranslation("ldapWritten") + " " + ServiceManager.getUserService().getFullName(user)); /* * check if HomeDir exists, else create it */ logger.debug("HomeVerzeichnis pruefen"); URI homePath = getUserHomeDirectory(user); if (!new File(homePath).exists()) { logger.debug("HomeVerzeichnis existiert noch nicht"); ServiceManager.getFileService().createDirectoryForUser(homePath, user.getLogin()); logger.debug("HomeVerzeichnis angelegt"); } else { logger.debug("HomeVerzeichnis existiert schon"); } } else { Helper.setMessage("ldapIsReadOnly"); } } /** * Check if connection with login and password possible. * * @param user * User object * @param password * String * @return Login correct or not */ public boolean isUserPasswordCorrect(User user, String password) { logger.debug("start login session with ldap"); Hashtable<String, String> env = initializeWithLdapConnectionSettings(user.getLdapGroup().getLdapServer()); // Start TLS if (ConfigCore.getBooleanParameterOrDefaultValue(ParameterCore.LDAP_USE_TLS)) { logger.debug("use TLS for auth"); return isPasswordCorrectForAuthWithTLS(env, user, password); } else { logger.debug("don't use TLS for auth"); return isPasswordCorrectForAuthWithoutTLS(env, user, password); } } /** * Retrieve home directory of given user. * * @param user * User object * @return path as URI */ public URI getUserHomeDirectory(User user) { String userFolderBasePath = ConfigCore.getParameter(ParameterCore.DIR_USERS); if (ConfigCore.getBooleanParameterOrDefaultValue(ParameterCore.LDAP_USE_LOCAL_DIRECTORY)) { return Paths.get(userFolderBasePath, user.getLogin()).toUri(); } Hashtable<String, String> env = initializeWithLdapConnectionSettings(user.getLdapGroup().getLdapServer()); if (ConfigCore.getBooleanParameterOrDefaultValue(ParameterCore.LDAP_USE_TLS)) { return getUserHomeDirectoryWithTLS(env, userFolderBasePath, user); } if (ConfigCore.getBooleanParameter(ParameterCore.LDAP_USE_SIMPLE_AUTH, false)) { env.put(Context.SECURITY_AUTHENTICATION, "none"); } DirContext ctx; URI userFolderPath = null; try { ctx = new InitialDirContext(env); Attributes attrs = ctx.getAttributes(buildUserDN(user)); Attribute ldapAttribute = attrs.get("homeDirectory"); userFolderPath = URI.create((String) ldapAttribute.get(0)); ctx.close(); } catch (NamingException e) { logger.error(e.getMessage(), e); } if (Objects.nonNull(userFolderPath) && !userFolderPath.isAbsolute()) { if (userFolderPath.getPath().startsWith("/")) { userFolderPath = ServiceManager.getFileService().deleteFirstSlashFromPath(userFolderPath); } return Paths.get(userFolderBasePath, userFolderPath.getRawPath()).toUri(); } else { return userFolderPath; } } /** * Check if User already exists on system. * * @param user * The User. * @return result as boolean */ public boolean isUserAlreadyExists(User user) { Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings( user.getLdapGroup().getLdapServer()); DirContext ctx; boolean result = false; try { ctx = new InitialDirContext(ldapEnvironment); Attributes matchAttrs = new BasicAttributes(true); NamingEnumeration<SearchResult> answer = ctx.search(buildUserDN(user), matchAttrs); result = answer.hasMoreElements(); while (answer.hasMore()) { SearchResult sr = answer.next(); logger.debug(">>>{}", sr.getName()); Attributes attrs = sr.getAttributes(); String givenName = getStringForAttribute(attrs, "givenName"); String surName = getStringForAttribute(attrs, "sn"); String mail = getStringForAttribute(attrs, "mail"); String cn = getStringForAttribute(attrs, "cn"); String homeDirectory = getStringForAttribute(attrs, "homeDirectory"); logger.debug(givenName); logger.debug(surName); logger.debug(mail); logger.debug(cn); logger.debug(homeDirectory); } ctx.close(); } catch (NamingException e) { logger.error(e.getMessage(), e); } return result; } private String getStringForAttribute(Attributes attrs, String identifier) { try { return attrs.get(identifier).toString(); } catch (RuntimeException e) { return " "; } } /** * Get next free uidNumber. * * @return next free uidNumber */ private String getNextUidNumber(LdapServer ldapServer) { Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings(ldapServer); DirContext ctx; String rueckgabe = ""; try { ctx = new InitialDirContext(ldapEnvironment); Attributes attrs = ctx.getAttributes(ldapServer.getNextFreeUnixIdPattern()); Attribute la = attrs.get("uidNumber"); rueckgabe = (String) la.get(0); ctx.close(); } catch (NamingException e) { Helper.setErrorMessage(e.getMessage(), logger, e); } return rueckgabe; } /** * Set next free uidNumber. */ private void setNextUidNumber(LdapServer ldapServer) { Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings(ldapServer); DirContext ctx; try { ctx = new InitialDirContext(ldapEnvironment); Attributes attrs = ctx.getAttributes(ldapServer.getNextFreeUnixIdPattern()); Attribute la = attrs.get("uidNumber"); String oldValue = (String) la.get(0); int bla = Integer.parseInt(oldValue) + 1; BasicAttribute attrNeu = new BasicAttribute("uidNumber", String.valueOf(bla)); ModificationItem[] mods = new ModificationItem[1]; mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attrNeu); ctx.modifyAttributes(ldapServer.getNextFreeUnixIdPattern(), mods); ctx.close(); } catch (NamingException e) { logger.error(e.getMessage(), e); } } /** * change password of given user, needs old password for authentication. * * @param user * User object * @param inNewPassword * String * @return boolean about result of change */ public boolean changeUserPassword(User user, String inNewPassword) throws NoSuchAlgorithmException { JDKMessageDigest.MD4 digester = new JDKMessageDigest.MD4(); PasswordEncryption passwordEncryption = user.getLdapGroup().getLdapServer().getPasswordEncryption(); Hashtable<String, String> env = initializeWithLdapConnectionSettings(user.getLdapGroup().getLdapServer()); if (!user.getLdapGroup().getLdapServer().isReadOnly()) { try { ModificationItem[] mods = new ModificationItem[4]; // encryption of password and Base64-Encoding MessageDigest md = MessageDigest.getInstance(passwordEncryption.getTitle()); md.update(inNewPassword.getBytes(StandardCharsets.UTF_8)); String encryptedPassword = new String(Base64.encodeBase64(md.digest()), StandardCharsets.UTF_8); // change attribute userPassword BasicAttribute userPassword = new BasicAttribute("userPassword", "{" + passwordEncryption + "}" + encryptedPassword); mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userPassword); // change attribute lanmgrPassword BasicAttribute lanmgrPassword = proceedPassword("sambaLMPassword", inNewPassword, null); mods[1] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, lanmgrPassword); // change attribute ntlmPassword BasicAttribute ntlmPassword = proceedPassword("sambaNTPassword", inNewPassword, digester); mods[2] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, ntlmPassword); BasicAttribute sambaPwdLastSet = new BasicAttribute("sambaPwdLastSet", String.valueOf(System.currentTimeMillis() / 1000L)); mods[3] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, sambaPwdLastSet); DirContext ctx = new InitialDirContext(env); ctx.modifyAttributes(buildUserDN(user), mods); // Close the context when we're done ctx.close(); return true; } catch (NamingException e) { logger.debug("Benutzeranmeldung nicht korrekt oder Passwortnderung nicht mglich", e); return false; } } return false; } private URI getUserHomeDirectoryWithTLS(Hashtable<String, String> env, String userFolderBasePath, User user) { env.put("java.naming.ldap.version", "3"); LdapContext ctx = null; StartTlsResponse tls = null; try { ctx = new InitialLdapContext(env, null); // Authentication must be performed over a secure channel tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest()); tls.negotiate(); ctx.reconnect(null); Attributes attrs = ctx.getAttributes(buildUserDN(user)); Attribute la = attrs.get("homeDirectory"); return URI.create((String) la.get(0)); } catch (IOException e) { logger.error("TLS negotiation error:", e); return Paths.get(userFolderBasePath, user.getLogin()).toUri(); } catch (NamingException e) { logger.error("JNDI error:", e); return Paths.get(userFolderBasePath, user.getLogin()).toUri(); } finally { closeConnections(ctx, tls); } } private boolean isPasswordCorrectForAuthWithTLS(Hashtable<String, String> env, User user, String password) { env.put("java.naming.ldap.version", "3"); LdapContext ctx = null; StartTlsResponse tls = null; try { ctx = new InitialLdapContext(env, null); // Authentication must be performed over a secure channel tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest()); tls.negotiate(); // Authenticate via SASL EXTERNAL mechanism using client X.509 // certificate contained in JVM keystore ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, buildUserDN(user)); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); ctx.reconnect(null); return true; // perform search for privileged attributes under authenticated context } catch (IOException e) { logger.error("TLS negotiation error:", e); return false; } catch (NamingException e) { logger.error("JNDI error:", e); return false; } finally { closeConnections(ctx, tls); } } private boolean isPasswordCorrectForAuthWithoutTLS(Hashtable<String, String> env, User user, String password) { if (ConfigCore.getBooleanParameter(ParameterCore.LDAP_USE_SIMPLE_AUTH, false)) { env.put(Context.SECURITY_AUTHENTICATION, "none"); // TODO: test for password } else { env.put(Context.SECURITY_PRINCIPAL, buildUserDN(user)); env.put(Context.SECURITY_CREDENTIALS, password); } logger.debug("ldap environment set"); try { logger.debug("start classic ldap authentication"); logger.debug("user DN is {}", buildUserDN(user)); if (Objects.isNull(ConfigCore.getParameter(ParameterCore.LDAP_ATTRIBUTE_TO_TEST))) { logger.debug("ldap attribute to test is null"); DirContext ctx = new InitialDirContext(env); ctx.close(); return true; } else { logger.debug("ldap attribute to test is not null"); DirContext ctx = new InitialDirContext(env); Attributes attrs = ctx.getAttributes(buildUserDN(user)); Attribute la = attrs.get(ConfigCore.getParameter(ParameterCore.LDAP_ATTRIBUTE_TO_TEST)); logger.debug("ldap attributes set"); String test = (String) la.get(0); if (test.equals(ConfigCore.getParameter(ParameterCore.LDAP_VALUE_OF_ATTRIBUTE))) { logger.debug("ldap ok"); ctx.close(); return true; } else { logger.debug("ldap not ok"); ctx.close(); return false; } } } catch (NamingException e) { logger.debug("login not allowed for {}. Exception: {}", user.getLogin(), e); return false; } } private void closeConnections(LdapContext ctx, StartTlsResponse tls) { if (Objects.nonNull(tls)) { try { // Tear down TLS connection tls.close(); } catch (IOException e) { logger.error(e.getMessage(), e); } } if (Objects.nonNull(ctx)) { try { // Close LDAP connection ctx.close(); } catch (NamingException e) { logger.error(e.getMessage(), e); } } } private BasicAttribute proceedPassword(String identifier, String newPassword, JDKMessageDigest.MD4 digester) { try { byte[] hash; if (Objects.isNull(digester)) { hash = LdapUser.lmHash(newPassword); } else { hash = digester.digest(newPassword.getBytes(StandardCharsets.UTF_16LE)); } return new BasicAttribute(identifier, LdapUser.toHexString(hash)); // TODO: Don't catch super class exception, make sure that // the password isn't logged here } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | RuntimeException e) { logger.error(e.getMessage(), e); return null; } } // TODO test if this methods works private void loadCertificates(String path, String passwd, LdapServer ldapServer) { /* wenn die Zertifikate noch nicht im Keystore sind, jetzt einlesen */ File myPfad = new File(path); if (!myPfad.exists()) { try (FileOutputStream ksos = (FileOutputStream) ServiceManager.getFileService().write(myPfad.toURI()); // TODO: Rename parameters to something more meaningful, // this is quite specific for the GDZ FileInputStream cacertFile = new FileInputStream(ldapServer.getRootCertificate()); FileInputStream certFile2 = new FileInputStream(ldapServer.getPdcCertificate())) { CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cacert = (X509Certificate) cf.generateCertificate(cacertFile); X509Certificate servercert = (X509Certificate) cf.generateCertificate(certFile2); KeyStore ks = KeyStore.getInstance("jks"); char[] password = passwd.toCharArray(); // TODO: Let this method really load a keystore if configured // initialize the keystore, if file is available, load the // keystore ks.load(null); ks.setCertificateEntry("ROOTCERT", cacert); ks.setCertificateEntry("PDC", servercert); ks.store(ksos, password); } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | RuntimeException e) { logger.error(e.getMessage(), e); } } } }