/*******************************************************************************
* gMix open source project - https://svs.informatik.uni-hamburg.de/gmix/
* Copyright (C) 2014 SVS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package userGeneratedContent.testbedPlugIns.layerPlugIns.layer2recodingScheme.encDNS_v0_001;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import org.apache.commons.lang3.ArrayUtils;
import staticContent.framework.EncDnsClient;
import staticContent.framework.controller.Implementation;
import staticContent.framework.interfaces.Layer1NetworkClient;
import staticContent.framework.interfaces.Layer2RecodingSchemeClient;
import staticContent.framework.interfaces.Layer3OutputStrategyClient;
import staticContent.framework.interfaces.Layer4TransportClient;
import staticContent.framework.message.Reply;
import staticContent.framework.message.Request;
import userGeneratedContent.testbedPlugIns.layerPlugIns.layer5application.encDNS_v0_001.EncDnsReply;
public class ClientPlugIn extends Implementation implements Layer2RecodingSchemeClient {
@Override
public void constructor() {
// nothing to do here
}
@Override
public void initialize() {
// nothing to do here
}
@Override
public void begin() {
// nothing to do here
}
@Override
public void setReferences(
Layer1NetworkClient layer1,
Layer2RecodingSchemeClient layer2,
Layer3OutputStrategyClient layer3,
Layer4TransportClient layer4) {
assert layer2 == this;
}
@Override
public Request applyLayeredEncryption(Request request) {
byte[] encMsg;
if(EncDnsClient.encryption) {
// If encryption is enabled, encrypt message
encMsg = encryptQuery(request.getByteMessage());
} else {
// Else just use the unencrypted message (copy)
encMsg = Arrays.copyOf(request.getByteMessage(), request.getByteMessage().length);
}
if (encMsg == null) {
if(EncDnsClient.verbosity >= 1)
System.out.println("Received empty response");
return null;
}
request.setByteMessage(encMsg);
return request;
}
/**
* Encrypts a standard DNS query.
* @param rcvDNS standard DNS query
* @return encrypted EncDNS message containing the standard DNS query
*/
private byte[] encryptQuery(byte[] rcvDNS) {
// Generate client nonce: 8 byte timer+4 byte random value
byte[] n = new byte[EncDnsClient.libSodium.NONCEBYTES];
ByteBuffer timeBuffer = ByteBuffer.allocate(8);
timeBuffer.putLong(System.nanoTime());
byte[] time = timeBuffer.array();
byte[] random = new byte[(EncDnsClient.libSodium.NONCEBYTES/2)-time.length];
EncDnsClient.rand.nextBytes(random);
System.arraycopy(time, 0, n, 0, time.length);
System.arraycopy(random, 0, n, time.length, random.length);
long ctime = System.currentTimeMillis();
byte[] cbox = EncDnsClient.libSodium.makeCryptoBoxAfternm(rcvDNS, EncDnsClient.k, n);
if(EncDnsClient.verbosity >= 1)
System.out.println("query encryption time " + (System.currentTimeMillis()-ctime) + " ms");
byte[] encQuery = buildEncQuery(cbox, n);
return encQuery;
}
/**
* Constructs an EncDNS message containing a specified cryptobox.
* @param cbox cryptobox to include in message
* @param nonce nonce used for encryption
* @return EncDNS message containing the specified cryptobox and nonce
*/
private byte[] buildEncQuery(byte[] cbox, byte[] nonce) {
// TODO This uses 65 kB of memory even if the resultig query is a lot
// smaller ... :-(
ByteBuffer buf = ByteBuffer.allocate(EncDnsClient.MAX_MSG_SIZE);
// ------ CBOX IN HOSTNAME ------
// --- DNS HEADER ---
// randomized ID
byte[] id = new byte[2];
EncDnsClient.rand.nextBytes(id);
buf.put(id);
buf.put((byte) 1); // QR=0 (Query), OpCode=0000 (Query), AA=0, TC=0, RD=1
buf.put((byte) 0); // RA,Z,AD,CD,RCode=0
buf.putShort((short) 1); // question count = 1
buf.putShort((short) 0); // answer count = 0
buf.putShort((short) 0); // authority count = 0
buf.putShort((short) 1); // additional count = 1
// --- DNS QUESTION SECTION ---
// cbox must be split into labels of length<=63
int cpos = 0;
// We have to transmit: magic string, the client's public key, the client's part of the nonce and the cryptobox itself
byte[] cryptoStuff = ArrayUtils.addAll(EncDnsClient.magicString, ArrayUtils.addAll(ArrayUtils.addAll(EncDnsClient.pk, Arrays.copyOfRange(nonce, 0, (EncDnsClient.libSodium.NONCEBYTES/2))), cbox));
while (true) {
if ((cpos + 63) < cryptoStuff.length) {
// remaining part of cbox > 63 byte -> add first 63 bytes as label
buf.put((byte)63);
buf.put(cryptoStuff, cpos, 63);
cpos += 63;
} else {
// remaining part of cbox <= 63 byte -> add as last label
short remainingBytes = (short) (cryptoStuff.length - cpos);
buf.put((byte)remainingBytes);
buf.put(cryptoStuff, cpos, remainingBytes);
break;
}
}
buf.put(EncDnsClient.remoteNS); // put the remote recursive nameserver's EncDNS zone name at the end of the question name
buf.putShort((short) 16); // TYPE=TXT(16)
buf.putShort((short) 1); // CLASS=IN(1)
// --- DNS ADDITIONAL SECTION - EDNS0 OPT PSEUDO-RR ---
buf.put((byte) 0); // Domain name = root
buf.putShort((short) 41); // RR TYPE=OPT(41)
buf.putShort((short) EncDnsClient.MAX_MSG_SIZE); // sender max UDP payload
buf.put((byte) 0); // RCode extension=0
buf.put((byte) 0); // EDNS version=0
buf.put((byte) 0); // DNSSEC OK=0, Z=0
buf.put((byte) 0); // Z=0
buf.putShort((short) 0); // RDATA length
// convert to byte[]
byte[] out = new byte[buf.position()];
buf.position(0);
buf.get(out);
return out;
}
@Override
public Reply extractPayload(Reply replyMessage) {
EncDnsReply reply = (EncDnsReply)replyMessage;
byte[] ansReply = reply.getByteMessage();
byte[] decReply;
if (ansReply != null) {
byte[] qid = reply.id;
byte[] rid = Arrays.copyOfRange(ansReply, 0, 2);
// Compare query and response ID
if (Arrays.equals(qid, rid)) {
if(EncDnsClient.encryption) {
// If encryption enabled, we will have to decrypt the response
decReply = decryptResponse(ansReply);
} else {
// Otherwise we can just use the response as is
decReply = Arrays.copyOf(ansReply, ansReply.length);
}
} else {
// If the received message is not an EncDNS message or if it
// does not match the query, a server failure will be
// reported to the stub resolver and the port is closed.
// The correct response might still be received later, but
// keeping the port open would allow an attacker to try
// multiple forged responses for one query whereas closing
// the port only gives him a single chance at the cost of
// having to re-query if a stray package arrived at the port.
decReply = EncDNSHelper.generateServfail(reply.id);
}
if (decReply == null) {
// If the reply did not contain a decryptable answer,
// generate a SERVFAIL message to send to the stub
decReply = EncDNSHelper.generateServfail(reply.id);
}
} else {
// If the response is empty, a server failure will be reported
// back to the stub resolver.
decReply = EncDNSHelper.generateServfail(reply.id);
}
reply.setByteMessage(decReply);
return reply;
}
/**
* Decrypt a response received from the local recursive nameserver
* @param encResponse response received from the local recursive nameserver
* @return decrypted standard DNS message
*/
private byte[] decryptResponse(byte[] encResponse) {
if((encResponse[3]&0x0f)!=0) { // if RCODE!=0 (i.e. if an error occurred)
return null;
}
// find the position of the question name's end in the message
int qNameEnd = EncDNSHelper.findQuestionNameEnd(encResponse);
// This is a bit of cheating, but we know that the remote proxy should
// send only one answer - if there are additional questions or answers
// in the message, something went wrong (probably at the local recursive
// nameserver)...
// glue together <character-string>s of split messages
int txtlen = (((int) encResponse[qNameEnd + 15]) << 8) | (encResponse[qNameEnd+16]&0xff);
ArrayList<Byte> cryptList = new ArrayList<Byte>();
for (int mpos = 17; mpos < (17+txtlen);) {
int cslen = encResponse[qNameEnd+mpos]&0xff; // get length of character-string (and correct sign)
mpos++;
for (int i = 0; i < cslen; i++) {
cryptList.add(encResponse[qNameEnd+mpos]);
mpos++;
}
}
byte[] cryptoStuff = ArrayUtils.toPrimitive(cryptList.toArray(new Byte[0]));
byte[] n = Arrays.copyOfRange(cryptoStuff, 0, EncDnsClient.libSodium.NONCEBYTES); // nonce
byte[] cbox = Arrays.copyOfRange(cryptoStuff, EncDnsClient.libSodium.NONCEBYTES, cryptoStuff.length); // cryptobox
long time = System.currentTimeMillis();
// decrypt the message
byte[] decM = EncDnsClient.libSodium.openCryptoBoxAfternm(cbox, EncDnsClient.k, n);
if(EncDnsClient.verbosity >= 1)
System.out.println("response decryption time " + (System.currentTimeMillis()-time) + " ms");
return decM;
}
@Override
public int getMaxPayloadForNextMessage() {
return EncDnsClient.MAX_MSG_SIZE;
}
@Override
public int getMaxPayloadForNextReply() {
return EncDnsClient.MAX_MSG_SIZE;
}
}