From f4a28649423c65d1bb8aa5de6e6b85c17d862679 Mon Sep 17 00:00:00 2001 From: Zlatin Balevsky Date: Sat, 9 Nov 2019 16:39:16 +0000 Subject: [PATCH] add extended signature in queries to prevent replay attacks --- .../muwire/core/connection/Connection.groovy | 34 ++++++++++++++++++- .../com/muwire/core/search/QueryEvent.groovy | 2 ++ .../muwire/core/update/UpdateClient.groovy | 8 +++-- .../main/java/com/muwire/core/Constants.java | 2 ++ .../java/com/muwire/core/util/DataUtil.java | 10 ++++++ .../com/muwire/gui/MainFrameController.groovy | 12 +++++-- 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/core/src/main/groovy/com/muwire/core/connection/Connection.groovy b/core/src/main/groovy/com/muwire/core/connection/Connection.groovy index 7ae5ccc3..50e68234 100644 --- a/core/src/main/groovy/com/muwire/core/connection/Connection.groovy +++ b/core/src/main/groovy/com/muwire/core/connection/Connection.groovy @@ -153,6 +153,10 @@ abstract class Connection implements Closeable { query.originator = e.originator.toBase64() if (e.sig != null) query.sig = Base64.encode(e.sig) + if (e.queryTime > 0) + query.queryTime = e.queryTime + if (e.sig2 != null) + query.sig2 = Base64.encode(e.sig2) messages.put(query) } @@ -249,6 +253,32 @@ abstract class Connection implements Closeable { log.info("query signature verified") } else log.info("no signature in query") + + // TODO: make this mandatory at some point + byte[] sig2 = null + long queryTime = 0 + if (search.sig2 != null) { + if (search.queryTime == null) { + log.info("extended signature but no timestamp") + return + } + sig2 = Base64.decode(search.sig2) + queryTime = search.queryTime + byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII) + def spk = originator.destination.getSigningPublicKey() + def signature = new Signature(Constants.SIG_TYPE, sig2) + if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) { + log.info("extended signature didn't match uuid and timestamp") + return + } else { + log.info("extended query signature verified") + if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) { + log.info("query too old") + return + } + } + } else + log.info("no extended signature in query") SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords, searchHash : infohash, @@ -262,7 +292,9 @@ abstract class Connection implements Closeable { originator : originator, receivedOn : endpoint.destination, firstHop : search.firstHop, - sig : sig ) + sig : sig, + queryTime : queryTime, + sig2 : sig2 ) eventBus.publish(event) } diff --git a/core/src/main/groovy/com/muwire/core/search/QueryEvent.groovy b/core/src/main/groovy/com/muwire/core/search/QueryEvent.groovy index bb753bb5..0187b77f 100644 --- a/core/src/main/groovy/com/muwire/core/search/QueryEvent.groovy +++ b/core/src/main/groovy/com/muwire/core/search/QueryEvent.groovy @@ -13,6 +13,8 @@ class QueryEvent extends Event { Persona originator Destination receivedOn byte[] sig + long queryTime + byte[] sig2 String toString() { "searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" + diff --git a/core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy b/core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy index 251561ba..00248e04 100644 --- a/core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy +++ b/core/src/main/groovy/com/muwire/core/update/UpdateClient.groovy @@ -13,6 +13,7 @@ import com.muwire.core.files.FileSharedEvent import com.muwire.core.search.QueryEvent import com.muwire.core.search.SearchEvent import com.muwire.core.search.UIResultBatchEvent +import com.muwire.core.util.DataUtil import groovy.json.JsonOutput import groovy.json.JsonSlurper @@ -176,9 +177,12 @@ class UpdateClient { signer = payload.signer log.info("starting search for new version hash $payload.infoHash") Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk) - def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true, persona : me) + UUID uuid = UUID.randomUUID() + long timestamp = System.currentTimeMillis() + byte [] sig2 = DataUtil.signUUID(uuid, timestamp, spk) + def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : uuid, oobInfohash : true, persona : me) def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination, - receivedOn : me.destination, originator : me, sig : sig.data) + receivedOn : me.destination, originator : me, sig : sig.data, queryTime : timestamp, sig2 : sig2) eventBus.publish(queryEvent) } } diff --git a/core/src/main/java/com/muwire/core/Constants.java b/core/src/main/java/com/muwire/core/Constants.java index f29ff146..7c17236f 100644 --- a/core/src/main/java/com/muwire/core/Constants.java +++ b/core/src/main/java/com/muwire/core/Constants.java @@ -13,4 +13,6 @@ public class Constants { public static final int MAX_RESULTS = 0x1 << 16; public static final int MAX_COMMENT_LENGTH = 0x1 << 15; + + public static final long MAX_QUERY_AGE = 5 * 60 * 1000L; } diff --git a/core/src/main/java/com/muwire/core/util/DataUtil.java b/core/src/main/java/com/muwire/core/util/DataUtil.java index 3aa72146..b112ba35 100644 --- a/core/src/main/java/com/muwire/core/util/DataUtil.java +++ b/core/src/main/java/com/muwire/core/util/DataUtil.java @@ -15,11 +15,15 @@ import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import com.muwire.core.Constants; +import net.i2p.crypto.DSAEngine; import net.i2p.data.Base64; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; import net.i2p.util.ConcurrentHashSet; public class DataUtil { @@ -203,4 +207,10 @@ public class DataUtil { .collect(Collectors.joining(",")); props.setProperty(property, encoded); } + + public static byte[] signUUID(UUID uuid, long timestamp, SigningPrivateKey spk) { + byte [] payload = (uuid.toString() + String.valueOf(timestamp)).getBytes(StandardCharsets.US_ASCII); + Signature sig = DSAEngine.getInstance().sign(payload, spk); + return sig.getData(); + } } diff --git a/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy b/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy index 0f551e5a..6e40d929 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy @@ -7,9 +7,11 @@ import griffon.core.mvc.MVCGroup import griffon.core.mvc.MVCGroupConfiguration import griffon.inject.MVCMember import griffon.metadata.ArtifactProviderFor +import groovy.json.StringEscapeUtils import net.i2p.crypto.DSAEngine import net.i2p.data.Base64 import net.i2p.data.Signature +import net.i2p.data.SigningPrivateKey import java.awt.Desktop import java.awt.event.ActionEvent @@ -43,6 +45,7 @@ import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustSubscriptionEvent import com.muwire.core.upload.HashListUploader import com.muwire.core.upload.Uploader +import com.muwire.core.util.DataUtil @ArtifactProviderFor(GriffonController) class MainFrameController { @@ -121,9 +124,10 @@ class MainFrameController { Signature sig = DSAEngine.getInstance().sign(payload, core.spk) + long timestamp = System.currentTimeMillis() core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop, replyTo: core.me.destination, receivedOn: core.me.destination, - originator : core.me, sig : sig.data)) + originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk))) } @@ -140,14 +144,16 @@ class MainFrameController { byte [] infoHashBytes = Base64.decode(infoHash) Signature sig = DSAEngine.getInstance().sign(infoHashBytes, core.spk) + long timestamp = System.currentTimeMillis() + byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk) def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid, oobInfohash: true, persona : core.me) core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo: core.me.destination, receivedOn: core.me.destination, - originator : core.me, sig : sig.data)) + originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2)) } - + private int selectedDownload() { def downloadsTable = builder.getVariable("downloads-table") def selected = downloadsTable.getSelectedRow()