package org.jgroups.auth; import org.jgroups.Address; import org.jgroups.Message; import org.jgroups.annotations.Property; import org.jgroups.protocols.ASYM_ENCRYPT; import org.jgroups.protocols.AUTH; import org.jgroups.util.Util; import java.io.DataInput; import java.io.DataOutput; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; /** * AuthToken implementation which shows how to do challenge-response based authentication. This could for example be * used by a coordinator A to authenticate joiner D.<p/> * When D wants to join, it sends a join request to A. A sends a challenge to D, which transforms the challenge into a * response and sends the hash of the response back to A.<p/> * A uses the same algorithm to transform the challenge, hashes the result and compares it to the hash received by * D. If they're equal, D is authenticated successfully.<p/> * The algorithm to transform the challenge into a response is {@link #encrypt(byte[])}.<p/ * To prevent man-in-the-middle attacks, the public key of A is sent with the challenge and D encrypts the response * with it. On reception of the response, A uses its private key to decrypt it.<p/> * Note that challenge-response authentication does not prevent MIM attacks, see * <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Wikipedia</a>. * @author Bela Ban * @since 3.3 */ @SuppressWarnings("unused") public class ChallengeResponseToken extends AuthToken implements AUTH.UpHandler { protected static final short ID=1555; // the ID to fetch a ChallengeResponseHeader from a message @Property(description="How long to wait (in ms) for a response to a challenge") protected long block_time=5000; @Property(description="Number of bytes in a challenge") protected int challenge_size=16; // Used to correlate pending challenge requests sent with responses received protected final Map<Address,Entry> pending_requests=new HashMap<>(); protected static final BiPredicate<Message,Boolean> BYPASSER_FUNCTION=(msg,up) -> { ChallengeResponseHeader hdr=msg.getHeader(ID); return hdr != null && (hdr.type == ChallengeResponseHeader.CHALLENGE || hdr.type == ChallengeResponseHeader.RESPONSE); }; public String getName() {return ChallengeResponseToken.class.getName();} public void init() { auth.register(this); registerBypasser(auth); } public void destroy() { super.destroy(); unregisterBypasser(auth); } public boolean authenticate(AuthToken token, Message msg) { Address sender=msg.getSrc(); // 1. send a challenge to the sender byte[] buf=generateRandomBytes(challenge_size); Message challenge=new Message(sender).setFlag(Message.Flag.OOB) .putHeader(ID, new ChallengeResponseHeader(buf)); Entry entry=new Entry(buf); pending_requests.put(sender, entry); // here we'd have to check if a latch already exists... log.trace("%s: sending challenge to %s", auth.getAddress(), sender); try { auth.getDownProtocol().down(challenge); long hash=entry.future.get(block_time, TimeUnit.MILLISECONDS); boolean result=hash > 0 && hash == hash(encrypt(entry.challenge)); log.trace("%s: authentication of %s: %b (hash=%d)", auth.getAddress(), sender, result, hash); return result; } catch(Exception e) { return false; } finally { pending_requests.remove(sender); } } public void writeTo(DataOutput out) throws Exception {} public void readFrom(DataInput in) throws Exception {} public int size() {return 0;} public boolean handleUpMessage(Message msg) { ChallengeResponseHeader hdr=msg.getHeader(ID); if(hdr == null) return true; switch(hdr.type) { case ChallengeResponseHeader.CHALLENGE: long hash=hash(encrypt(hdr.payload)); Message response=new Message(msg.getSrc()).setFlag(Message.Flag.OOB) .putHeader(ID, new ChallengeResponseHeader(hash)); log.trace("%s: received CHALLENGE from %s; sending RESPONSE (hash=%d)", auth.getAddress(), msg.src(), hash); auth.getDownProtocol().down(response); break; case ChallengeResponseHeader.RESPONSE: log.trace("%s: received RESPONSE from %s", auth.getAddress(), msg.getSrc()); Entry entry=pending_requests.get(msg.getSrc()); if(entry != null) entry.setResponse(hdr.hash); break; } return false; // don't pass up } protected static void registerBypasser(AUTH auth) { ASYM_ENCRYPT asym_encr=auth.getProtocolStack().findProtocol(ASYM_ENCRYPT.class); if(asym_encr != null) asym_encr.registerBypasser(BYPASSER_FUNCTION); } protected static void unregisterBypasser(AUTH auth) { ASYM_ENCRYPT asym_encr=auth.getProtocolStack().findProtocol(ASYM_ENCRYPT.class); if(asym_encr != null) asym_encr.unregisterBypasser(BYPASSER_FUNCTION); } protected static byte[] generateRandomBytes(int size) { byte[] retval=new byte[size]; // here we'd have to generate a buffer with random contents for(int i=0; i < retval.length; i++) retval[i]=(byte)Util.random(Byte.MAX_VALUE); return retval; } /** * Simplistic example of a transformation by incrementing each byte in the array. */ protected static byte[] encrypt(byte[] buf) { for(int i=0; i < buf.length; i++) buf[i]=(byte)(buf[i]+1); return buf; } // A real hash would have to be provided here... protected static long hash(byte[] buf) { long retval=0; for(int i=0; i < buf.length; i++) retval+=buf[i]; return retval; } protected static class Entry { protected final CompletableFuture<Long> future=new CompletableFuture<>(); protected final byte[] challenge; public Entry(byte[] challenge) { this.challenge=challenge; } public void setResponse(long hash) { future.complete(hash); } } }