| /*=============================================================================# |
| # Copyright (c) 2009, 2022 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.rj.server.srvext; |
| |
| import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert; |
| |
| import java.rmi.server.RemoteServer; |
| import java.rmi.server.ServerNotActiveException; |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.SecureRandom; |
| import java.util.Locale; |
| import java.util.Random; |
| import java.util.logging.Level; |
| import java.util.logging.LogRecord; |
| import java.util.logging.Logger; |
| |
| import javax.security.auth.callback.Callback; |
| import javax.security.auth.callback.ChoiceCallback; |
| import javax.security.auth.callback.ConfirmationCallback; |
| import javax.security.auth.callback.LanguageCallback; |
| import javax.security.auth.callback.NameCallback; |
| import javax.security.auth.callback.PasswordCallback; |
| import javax.security.auth.callback.TextInputCallback; |
| import javax.security.auth.callback.TextOutputCallback; |
| import javax.security.auth.callback.UnsupportedCallbackException; |
| import javax.security.auth.login.FailedLoginException; |
| import javax.security.auth.login.LoginException; |
| |
| import org.eclipse.statet.jcommons.collections.ImList; |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| |
| import org.eclipse.statet.rj.RjException; |
| import org.eclipse.statet.rj.RjInitFailedException; |
| import org.eclipse.statet.rj.server.ServerLogin; |
| |
| |
| /** |
| * Abstract class for authentication methods. |
| * |
| * An authentication method must extend this class and implement the |
| * abstract methods: |
| * <li>{@link #doInit(String)}</li> |
| * <li>{@link #doCreateLogin()}</li> |
| * <li>{@link #doPerformLogin(Callback[])}</li> |
| */ |
| @NonNullByDefault |
| public abstract class ServerAuthMethod { |
| |
| |
| protected static void copyAnswer(final ImList<Callback> from, final Callback[] to) throws UnsupportedCallbackException { |
| if (from.size() != to.length) { |
| throw new IllegalArgumentException(); |
| } |
| for (int i= 0; i < from.size(); i++) { |
| final Callback source= from.get(i); |
| if (source instanceof TextOutputCallback) { |
| continue; |
| } |
| if (source instanceof NameCallback) { |
| ((NameCallback)to[i]).setName(((NameCallback)source).getName()); |
| continue; |
| } |
| if (source instanceof PasswordCallback) { |
| ((PasswordCallback)to[i]).setPassword(((PasswordCallback)source).getPassword()); |
| ((PasswordCallback)source).clearPassword(); |
| continue; |
| } |
| if (source instanceof TextInputCallback) { |
| ((TextInputCallback)to[i]).setText(((TextInputCallback)source).getText()); |
| continue; |
| } |
| if (source instanceof ChoiceCallback) { |
| final int[] selectedIndexes= ((ChoiceCallback)source).getSelectedIndexes(); |
| if (((ChoiceCallback)source).allowMultipleSelections()) { |
| ((ChoiceCallback)to[i]).setSelectedIndexes(selectedIndexes); |
| } |
| else if (selectedIndexes.length == 1) { |
| ((ChoiceCallback)to[i]).setSelectedIndex(selectedIndexes[0]); |
| } |
| continue; |
| } |
| if (source instanceof ConfirmationCallback) { |
| ((ConfirmationCallback)to[i]).setSelectedIndex(((ConfirmationCallback)source).getSelectedIndex()); |
| continue; |
| } |
| if (source instanceof LanguageCallback) { |
| ((LanguageCallback)to[i]).setLocale(Locale.getDefault()); |
| continue; |
| } |
| throw new UnsupportedCallbackException(to[i]); |
| } |
| } |
| |
| |
| private static final Logger LOGGER= Logger.getLogger("org.eclipse.statet.rj.server.auth"); |
| private final String logPrefix; |
| |
| |
| private final String id; |
| |
| private final Random randomGenerator= new SecureRandom(); |
| |
| private final boolean usePubkeyExchange; |
| private @Nullable KeyPairGenerator keyPairGenerator; |
| |
| private @Nullable String pendingLoginClient; |
| private long pendingLoginId; |
| private @Nullable KeyPair pendingLoginKeyPair; |
| |
| private @Nullable String expliciteClient; |
| |
| private boolean isInitialized; |
| |
| |
| /** |
| * |
| * @param usePubkeyExchange enables default encryption of secret data (password) |
| */ |
| protected ServerAuthMethod(final String id, final boolean usePubkeyExchange) { |
| this.usePubkeyExchange= usePubkeyExchange; |
| this.id= id; |
| this.logPrefix= "[Auth:" + id + "]"; |
| } |
| |
| |
| public void setExpliciteClient(final String client) { |
| this.expliciteClient= client; |
| } |
| |
| private String getCallingClient() throws ServerNotActiveException { |
| final String expliciteClient= this.expliciteClient; |
| if (expliciteClient != null) { |
| return expliciteClient; |
| } |
| return RemoteServer.getClientHost(); |
| } |
| |
| public boolean isValid(final Client client) { |
| try { |
| return (getCallingClient().equals(client.clientId)); |
| } |
| catch (final ServerNotActiveException e) { |
| return false; |
| } |
| } |
| |
| public final void init(final @Nullable String arg) throws RjException { |
| try { |
| if (this.usePubkeyExchange) { |
| final KeyPairGenerator keyPairGenerator= KeyPairGenerator.getInstance("RSA"); |
| keyPairGenerator.initialize(2048); |
| this.keyPairGenerator= keyPairGenerator; |
| } |
| |
| doInit(arg); |
| this.isInitialized= true; |
| } |
| catch (final Exception e) { |
| final RjException rje= (e instanceof RjException) ? (RjException) e : |
| new RjInitFailedException("An error occurred when initializing authentication method '"+this.id+"'.", e); |
| throw rje; |
| } |
| finally { |
| System.gc(); |
| } |
| } |
| |
| protected final Random getRandom() { |
| return this.randomGenerator; |
| } |
| |
| protected final void assertInitialized() { |
| if (!this.isInitialized) { |
| throw new IllegalStateException("not initialized"); |
| } |
| } |
| |
| /** |
| * Is called when the server is started |
| * |
| * @param arg configuration argument |
| * |
| * @throws RjException |
| */ |
| protected abstract void doInit(@Nullable String arg) throws RjException; |
| |
| public final ServerLogin createLogin() throws RjException { |
| assertInitialized(); |
| |
| try { |
| final String client= getCallingClient(); |
| final boolean same= client.equals(this.pendingLoginClient); |
| this.pendingLoginClient= client; |
| |
| LOGGER.log(Level.INFO, "{0} creating new login ({1}).", |
| new Object[] { this.logPrefix, client }); |
| |
| long nextId; |
| do { |
| nextId= this.randomGenerator.nextLong(); |
| } while (nextId == this.pendingLoginId); |
| this.pendingLoginId= nextId; |
| if (this.usePubkeyExchange) { |
| if (this.pendingLoginKeyPair == null || !same) { |
| final KeyPairGenerator keyPairGenerator= nonNullAssert(this.keyPairGenerator); |
| this.pendingLoginKeyPair= nonNullAssert(keyPairGenerator).generateKeyPair(); |
| } |
| } |
| |
| return createNewLogin(doCreateLogin()); |
| } |
| catch (final Exception e) { |
| final RjException rje= (e instanceof RjException) ? (RjException) e : |
| new RjException("An unexpected error occurred when preparing login process.", e); |
| throw rje; |
| } |
| } |
| |
| /** |
| * Is called when client initiates a login process. |
| * |
| * @return login callbacks to handle by the client |
| * |
| * @throws RjException |
| */ |
| protected abstract ImList<Callback> doCreateLogin() throws RjException; |
| |
| private ServerLogin createNewLogin(final ImList<Callback> callbacks) { |
| if (this.usePubkeyExchange) { |
| final KeyPair loginKeyPair= nonNullAssert(this.pendingLoginKeyPair); |
| return new ServerLogin(this.pendingLoginId, loginKeyPair.getPublic(), callbacks); |
| } |
| else { |
| return new ServerLogin(this.pendingLoginId, null, callbacks); |
| } |
| } |
| |
| public final Client performLogin(final ServerLogin login) throws RjException, LoginException { |
| assertInitialized(); |
| |
| String client= null; |
| try { |
| client= getCallingClient(); |
| if (login.getId() != this.pendingLoginId || |
| !client.equals(this.pendingLoginClient)) { |
| throw new FailedLoginException("Login process was interrupted by another client."); |
| } |
| |
| if (this.usePubkeyExchange) { |
| final KeyPair loginKeyPair= nonNullAssert(this.pendingLoginKeyPair); |
| login.readAnswer(loginKeyPair.getPrivate()); |
| } |
| else { |
| login.readAnswer(null); |
| } |
| this.pendingLoginKeyPair= null; |
| final String name= doPerformLogin(login.getCallbacks()); |
| |
| LOGGER.log(Level.INFO, "{0} performing login completed successfull: {1} ({2}).", |
| new Object[] { this.logPrefix, name, client }); |
| return new Client(name, getCallingClient(), (byte)0); |
| } |
| catch (final LoginException e) { |
| final LogRecord log= new LogRecord(Level.INFO, "{0} performing login failed ({1})."); |
| log.setParameters(new @Nullable Object[] { this.logPrefix, client }); |
| log.setThrown(e); |
| LOGGER.log(log); |
| throw e; |
| } |
| catch (final RjException e) { |
| throw e; |
| } |
| catch (final Exception e) { |
| throw new RjException("An unexpected error occurred when validating the login credential.", e); |
| } |
| finally { |
| System.gc(); |
| } |
| } |
| |
| /** |
| * This method is called when the client sends the login data |
| * |
| * @param callbacks the callbacks handled by the client (note, the callbacks are not |
| * the same instances returned by {@link #createLogin()}, but clones) |
| * |
| * @return login id like username |
| * |
| * @throws LoginException if login failed (but can usually fixed by other login data) |
| * @throws RjException |
| */ |
| protected abstract String doPerformLogin(ImList<Callback> callbacks) throws LoginException, RjException; |
| |
| } |