blob: b6e179dd24704bbc405986f5135af86a335cba63 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2021 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;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNull;
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.server.srvext.ServerAuthMethod;
@NonNullByDefault
public final class ServerLogin implements Serializable {
private static final long serialVersionUID= -596748668244272719L;
private long id;
private @Nullable Key pubkey;
private Callback @Nullable [] callbacks;
public ServerLogin(final long id, final @Nullable Key pubkey, final ImList<Callback> callbacks) {
this.id= id;
this.pubkey= pubkey;
this.callbacks= callbacks.toArray(new @NonNull Callback[callbacks.size()]);
}
/** for Serializable */
public ServerLogin() {
}
public long getId() {
return this.id;
}
/**
* The callback items which should be handled by the client.
*
* @return an array with all callback objects
* @see CallbackHandler
*/
public ImList<Callback> getCallbacks() {
final var callbacks= this.callbacks;
if (callbacks == null) {
throw new IllegalStateException("no longer valid");
}
return ImCollections.newList(callbacks);
}
/**
* Must be called by the client to create login data for authentification
* when connecting to server via {@link Server#start(ServerLogin, String[])}
* or {@link Server#connect(ServerLogin)}.
*
* @return the login data prepared to send to the server
* @throws RjException when creating login data failed
*/
public ServerLogin createAnswer() throws RjException {
try {
final var callbacks= nonNullAssert(this.callbacks);
final var answerCallbacks= new @NonNull Callback[callbacks.length];
System.arraycopy(callbacks, 0, answerCallbacks, 0, callbacks.length);
final Key pubkey= this.pubkey;
if (pubkey != null) {
process(answerCallbacks, Cipher.ENCRYPT_MODE, pubkey);
}
return new ServerLogin(this.id, null, ImCollections.newList(answerCallbacks));
}
catch (final Exception e) {
throw new RjException("An error occurred when creating login data.", e);
}
}
/**
* Is called by server to decrypt data. By default it is called in
* {@link ServerAuthMethod#performLogin(ServerLogin)}.
*
* @param privateKey the key to decrypt the data
* @throws RjException when processing login data failed
*/
public void readAnswer(final @Nullable Key privateKey) throws RjException {
try {
final var callbacks= nonNullAssert(this.callbacks);
if (privateKey != null) {
process(callbacks, Cipher.DECRYPT_MODE, privateKey);
}
}
catch (final Exception e) {
throw new RjException("An error occurred when processing login data.", e);
}
}
private void process(final Callback[] callbacks, final int mode, final Key key) throws Exception {
final Cipher with= Cipher.getInstance("RSA");
with.init(mode, key);
final Charset charset= StandardCharsets.UTF_8;
for (int i= 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof PasswordCallback) {
final PasswordCallback c= (PasswordCallback) callbacks[i];
final char[] orgPassword= c.getPassword();
if (orgPassword != null) {
final byte[] orgBytes;
if (mode == Cipher.ENCRYPT_MODE) {
orgBytes= charset.encode(CharBuffer.wrap(orgPassword)).array();
}
else {
orgBytes= new byte[orgPassword.length];
for (int j= 0; j < orgBytes.length; j++) {
orgBytes[j]= (byte) orgPassword[j];
}
}
final byte[] encBytes= with.doFinal(orgBytes);
final char[] encPassword;
if (mode == Cipher.ENCRYPT_MODE) {
encPassword= new char[encBytes.length];
for (int j= 0; j < encPassword.length; j++) {
encPassword[j]= (char) encBytes[j];
}
}
else {
encPassword= charset.decode(ByteBuffer.wrap(encBytes)).array();
}
if (mode == Cipher.ENCRYPT_MODE) {
final PasswordCallback copy= new PasswordCallback(c.getPrompt(), c.isEchoOn());
copy.setPassword(encPassword);
callbacks[i]= copy;
}
else {
c.clearPassword();
c.setPassword(encPassword);
}
Arrays.fill(orgBytes, (byte)0);
Arrays.fill(orgPassword, (char)0);
Arrays.fill(encBytes, (byte)0);
Arrays.fill(encPassword, (char)0);
}
continue;
}
}
}
/**
* Clear data, especially the password.
*/
public void clearData() {
this.pubkey= null;
final var callbacks= this.callbacks;
if (callbacks != null) {
this.callbacks= null;
for (int i= 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback)callbacks[i]).clearPassword();
}
}
}
}
}