package plugins.CENO.Client.Signaling;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.security.GeneralSecurityException;
import java.util.concurrent.TimeUnit;
import plugins.CENO.Client.CENOClient;
import plugins.CENO.Common.Crypto;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.async.BaseClientPutter;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientPutCallback;
import freenet.client.async.PersistenceDisabledException;
import freenet.keys.FreenetURI;
import freenet.node.FSParseException;
import freenet.node.RequestClient;
import freenet.support.IllegalBase64Exception;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.api.Bucket;
import freenet.support.io.ResumeFailedException;
public class ChannelMaker implements Runnable {
private static final int MAX_KSK_POLLS = 20;
private FreenetURI signalSSK;
private FreenetURI signalSSKpub;
private boolean channelEstablished = false;
private ChannelStatus channelStatus = ChannelStatus.starting;
private long lastSynced = 0L;
public ChannelMaker() {
this(null, 0L);
}
public ChannelMaker(String signalSSKString, long lastSynced) {
this.lastSynced = lastSynced;
try {
if (signalSSKString != null) {
this.signalSSK = new FreenetURI(signalSSKString);
this.signalSSKpub = this.signalSSK.deriveRequestURIFromInsertURI();
this.lastSynced = lastSynced;
channelStatus = ChannelStatus.publishedKSK;
Logger.warning(this, "Retrieved previously established channel from client.properties");
if (System.currentTimeMillis() - lastSynced < TimeUnit.DAYS.toMillis(15)) {
channelStatus = ChannelStatus.waitingForSyn;
Logger.warning(this, "The established channel was synced less than 15 days ago and will start using without re-establishment");
}
return;
}
} catch (MalformedURLException e) {
Logger.error(this, "signalSSK read from configuration is not a valid Freenet key");
}
FreenetURI newKeyPair[] = CENOClient.nodeInterface.generateKeyPair();
this.signalSSK = newKeyPair[0];
this.signalSSKpub = newKeyPair[1];
this.channelStatus = ChannelStatus.starting;
Logger.warning(this, "Generated keys for establishing a secure channel");
}
@Override
public void run() {
if (!checkChannelEstablished()) {
if(ChannelStatus.isFatalStatus(channelStatus)) {
return;
}
establishChannel();
} else if (ChannelStatus.sentPrivUSK(channelStatus)) {
waitForSyn();
}
}
public String getSignalSSK() {
return signalSSK.toString();
}
public long getLastSynced() {
return lastSynced;
}
public boolean isFatal() {
return ChannelStatus.isFatalStatus(channelStatus);
}
public boolean canSend() {
return ChannelStatus.canSend(channelStatus);
}
public boolean sentPrivUSK() {
return ChannelStatus.sentPrivUSK(channelStatus);
}
private boolean checkChannelEstablished() {
if (System.currentTimeMillis() - lastSynced > TimeUnit.DAYS.toMillis(30)) {
return false;
}
if(ChannelStatus.sentPrivUSK(channelStatus)) {
return true;
}
return channelEstablished;
}
private boolean waitForSyn() {
FreenetURI synURI = new FreenetURI("USK", "syn", signalSSKpub.getRoutingKey(), signalSSKpub.getCryptoKey(), signalSSKpub.getExtra());
FetchResult fetchResult = null;
while(!channelEstablished) {
try {
fetchResult = CENOClient.nodeInterface.fetchURI(synURI);
} catch (FetchException e) {
if(e.getMode() == FetchExceptionMode.PERMANENT_REDIRECT) {
synURI = e.newURI;
} else if(e.isDNF() || e.isFatal()) {
try {
Thread.sleep(TimeUnit.MINUTES.toMillis(5));
} catch (InterruptedException e1) {}
continue;
}
}
if(fetchResult != null) {
try {
Long synDate = Long.parseLong(new String(fetchResult.asByteArray()));
if (System.currentTimeMillis() - synDate > TimeUnit.DAYS.toMillis(25)) {
establishChannel();
break;
}
} catch (IOException e) {
channelStatus = ChannelStatus.failedToParseSyn;
break;
}
Logger.normal(this, "Received syn from the bridge - signaling channel established successfully");
channelStatus = ChannelStatus.syn;
lastSynced = System.currentTimeMillis();
channelEstablished = true;
}
}
return channelEstablished;
}
private void establishChannel() {
FreenetURI bridgeKey;
try {
bridgeKey = new FreenetURI(CENOClient.getBridgeKey());
} catch (MalformedURLException e1) {
Logger.error(this, "Could not calculate the bridge key from the configured SSK: " + e1.getMessage());
channelStatus = ChannelStatus.fatal;
return;
}
FreenetURI bridgeSignalerURI = new FreenetURI("USK", "CENO-signaler", bridgeKey.getRoutingKey(), bridgeKey.getCryptoKey(), bridgeKey.getExtra());
FetchResult bridgeSignalFreesite = null;
while(bridgeSignalFreesite == null) {
try {
bridgeSignalFreesite = CENOClient.nodeInterface.fetchURI(bridgeSignalerURI);
} catch (FetchException e) {
if (e.mode == FetchException.FetchExceptionMode.PERMANENT_REDIRECT) {
bridgeSignalerURI = e.newURI;
continue;
}
if (e.isFatal()) {
channelStatus = ChannelStatus.failedToGetSignalSSK;
return;
}
Logger.warning(this, "Exception while retrieving the bridge's signal page: " + e.getMessage());
}
}
channelStatus = ChannelStatus.gotSignalSSK;
SimpleFieldSet sfs = null;
String question = null;
String pubKey = null;
try {
sfs = new SimpleFieldSet(new String(bridgeSignalFreesite.asByteArray()), false, true, true);
question = sfs.getString("question");
pubKey = sfs.getString("pubkey");
} catch (IOException e) {
Logger.error(this, "IOException while reading the CENO-signaler page");
} catch (FSParseException e) {
Logger.error(this, "Exception while parsing the SFS of the CENO-signaler");
} finally {
if (question == null || pubKey == null) {
// CENO Client won't be able to signal the bridge
//TODO Terminate plugin
channelStatus = ChannelStatus.failedToParseSignalSFS;
return;
}
}
channelStatus = ChannelStatus.puzzleSolved;
byte[] encReply = null;
try {
encReply = Crypto.encrypt(signalSSK.toString().getBytes("UTF-8"), pubKey);
} catch (GeneralSecurityException e1) {
Logger.error(this, "General security exception while encrypting response to quiz: " + e1.getMessage());
} catch (IllegalBase64Exception e1) {
Logger.error(this, "Could not base64 decrypt bridge's public key : " + e1.getMessage());
} catch (UnsupportedEncodingException e) {
Logger.error(this, "UTF-8 Encoding not supported");
} finally {
if (encReply == null) {
channelStatus = ChannelStatus.failedToEncrypt;
return;
}
}
insertSubKSK(question, encReply, new int[0]);
waitForSyn();
}
private void insertSubKSK(String question, byte[] encReply, int[] prevSubKSK) {
FreenetURI insertedKSK = null;
int randSubKSK = (int) (Math.random() * MAX_KSK_POLLS);
try {
insertedKSK = CENOClient.nodeInterface.insertSingleChunk(new FreenetURI("KSK@" + question + "-" + randSubKSK), encReply, new KSKSolutionPutCallback(question, encReply, new int[]{randSubKSK}));
} catch (PersistenceDisabledException e) {
Logger.error(this, "Tried to insert KSK reply with persistence, but persistence is disabled: " + e.getMessage());
channelStatus = ChannelStatus.failedToPublishKSK;
} catch (MalformedURLException e) {
Logger.error(this, "Reply KSK is malformed: " + e.getMessage());
channelStatus = ChannelStatus.failedToPublishKSK;
} catch (InsertException e) {
Logger.error(this, "Error while initializing insertion for KSK reply: " + e.getMessage());
channelStatus = ChannelStatus.failedToPublishKSK;
} catch (UnsupportedEncodingException e) {
Logger.error(this, "UTF-8 Encoding not supported");
channelStatus = ChannelStatus.failedToPublishKSK;
} finally {
if(insertedKSK == null) {
return;
}
}
channelStatus = ChannelStatus.publishedKSK;
Logger.normal(this, "Started publishing to KSK solution to the bridge slot " + randSubKSK);
return;
}
private class KSKSolutionPutCallback implements ClientPutCallback {
int[] prevSubKSK;
byte[] encReply;
String question;
public KSKSolutionPutCallback(String question, byte[] encReply, int[] prevSubKSK) {
this.question = question;
this.encReply = encReply;
this.prevSubKSK = prevSubKSK;
}
@Override
public void onResume(ClientContext context) throws ResumeFailedException {}
@Override
public RequestClient getRequestClient() {
return CENOClient.nodeInterface.getRequestClient();
}
@Override
public void onGeneratedURI(FreenetURI uri, BaseClientPutter state) {}
@Override
public void onGeneratedMetadata(Bucket metadata, BaseClientPutter state) {}
@Override
public void onFetchable(BaseClientPutter state) {}
@Override
public void onSuccess(BaseClientPutter state) {
Logger.normal(this,"Inserted private SSK key in the KSK@solution to the puzzle published by the bridge");
}
@Override
public void onFailure(InsertException e, BaseClientPutter state) {
if (e.getMode() == InsertExceptionMode.COLLISION) {
boolean subKSKUsed;
int randSubKSK;
do {
subKSKUsed = false;
randSubKSK = (int) (Math.random() * MAX_KSK_POLLS);
for (int i = 0; i < prevSubKSK.length ; i++) {
if (prevSubKSK[i] == randSubKSK) {
subKSKUsed = true;
break;
}
}
} while (subKSKUsed);
int[] subKSKUsedExt = new int[prevSubKSK.length + 1];
System.arraycopy(prevSubKSK,0, subKSKUsedExt, 0, prevSubKSK.length);
subKSKUsedExt[prevSubKSK.length] = randSubKSK;
insertSubKSK(question, encReply, subKSKUsedExt);
} else {
Logger.error(this, "Failed to publish KSK@solution: " + e.getMessage());
}
}
}
}