Compare commits

...

10 Commits

Author SHA1 Message Date
Zlatin Balevsky
fbf9add82a Release 0.6.0 2019-11-09 19:27:36 +00:00
Zlatin Balevsky
7379263fef extended signature in cli 2019-11-09 18:34:34 +00:00
Zlatin Balevsky
7d50843754 make signed queries mandatory 2019-11-09 17:03:38 +00:00
Zlatin Balevsky
f4a2864942 add extended signature in queries to prevent replay attacks 2019-11-09 16:39:16 +00:00
Zlatin Balevsky
afaadf65a4 only set selected row if the table contains that many rows. That fixes an AIOOBE 2019-11-09 15:14:14 +00:00
Zlatin Balevsky
7bd422d6b4 another instance of unexplained npe 2019-11-09 12:36:59 +00:00
Zlatin Balevsky
3f47274f61 add option to open containing folder 2019-11-09 11:28:12 +00:00
Zlatin Balevsky
419e9a0ce6 prevent npe when..? unclear when this happens 2019-11-09 11:01:55 +00:00
Zlatin Balevsky
ac1068a681 fix show comment/certificate buttons in group-by-file mode 2019-11-09 10:53:38 +00:00
Zlatin Balevsky
549457e36f close output stream silently 2019-11-08 21:46:44 +00:00
13 changed files with 114 additions and 20 deletions

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.5.10"
private static final String MW_VERSION = "0.6.0"
private static volatile Core core

View File

@@ -7,6 +7,7 @@ import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
@@ -45,13 +46,16 @@ class SearchModel {
def searchEvent
byte [] payload
UUID uuid = UUID.randomUUID()
long timestamp = System.currentTimeMillis()
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
payload = root
} else {
def nonEmpty = SplitPattern.termify(query)
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments, compressedResults : true)
}
@@ -61,7 +65,7 @@ class SearchModel {
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 : sig2))
}
void unregister() {

View File

@@ -406,7 +406,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.5.10")
Core core = new Core(props, home, "0.6.0")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -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)
}
@@ -232,7 +236,6 @@ abstract class Connection implements Closeable {
if (search.compressedResults != null)
compressedResults = search.compressedResults
byte[] sig = null
// TODO: make this mandatory at some point
if (search.sig != null) {
sig = Base64.decode(search.sig)
byte [] payload
@@ -247,8 +250,36 @@ abstract class Connection implements Closeable {
return
} else
log.info("query signature verified")
} else
} else {
log.info("no signature in query")
return
}
// 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 +293,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)
}

View File

@@ -159,7 +159,9 @@ class ConnectionAcceptor {
}
} catch (Exception ex) {
log.log(Level.WARNING, "incoming connection failed",ex)
e.getOutputStream().close()
try {
e.getOutputStream().close()
} catch (Exception ignore) {}
e.close()
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
}
@@ -208,7 +210,9 @@ class ConnectionAcceptor {
os.writeShort(json.bytes.length)
os.write(json.bytes)
}
e.outputStream.close()
try {
e.outputStream.close()
} catch (Exception ignored) {}
e.close()
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
}

View File

@@ -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()}" +

View File

@@ -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)
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.5.10
version = 0.6.0
i2pVersion = 0.9.43
groovyVersion = 2.4.15
slf4jVersion = 1.7.25

View File

@@ -7,10 +7,13 @@ 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
import java.nio.charset.StandardCharsets
@@ -42,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 {
@@ -120,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)))
}
@@ -139,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()
@@ -383,7 +390,7 @@ class MainFrameController {
@ControllerAction
void showFileDetails() {
def selected = view.selectedSharedFiles()
if (selected.size() != 1) {
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to view it's details")
return
}
@@ -392,6 +399,19 @@ class MainFrameController {
params['core'] = core
mvcGroup.createMVCGroup("shared-file", params)
}
@ControllerAction
void openContainingFolder() {
def selected = view.selectedSharedFiles()
if (selected == null || selected.size() != 1) {
JOptionPane.showMessageDialog(null, "Please select only one file to open it's containing folder")
return
}
try {
Desktop.getDesktop().open(selected[0].file.getParentFile())
} catch (Exception ignored) {}
}
void saveMuWireSettings() {
core.saveMuSettings()

View File

@@ -608,6 +608,9 @@ class MainFrameView {
JMenuItem certifySelectedFiles = new JMenuItem("Certify selected files")
certifySelectedFiles.addActionListener({mvcGroup.controller.issueCertificate()})
sharedFilesMenu.add(certifySelectedFiles)
JMenuItem openContainingFolder = new JMenuItem("Open containing folder")
openContainingFolder.addActionListener({mvcGroup.controller.openContainingFolder()})
sharedFilesMenu.add(openContainingFolder)
JMenuItem showFileDetails = new JMenuItem("Show file details")
showFileDetails.addActionListener({mvcGroup.controller.showFileDetails()})
sharedFilesMenu.add(showFileDetails)

View File

@@ -346,7 +346,8 @@ class SearchTabView {
model.senders2.addAll(results)
int selectedRow = sendersTable2.getSelectedRow()
sendersTable2.model.fireTableDataChanged()
sendersTable2.selectionModel.setSelectionInterval(selectedRow,selectedRow)
if (selectedRow < results.size())
sendersTable2.selectionModel.setSelectionInterval(selectedRow,selectedRow)
})
resultsTable2.addMouseListener(new MouseAdapter() {
@@ -367,7 +368,7 @@ class SearchTabView {
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int row = selectedSenderRow()
if (row < 0) {
if (row < 0 || model.senders2[row] == null) {
model.browseActionEnabled = false
model.viewCertificatesActionEnabled = false
model.trustButtonsEnabled = false
@@ -438,6 +439,17 @@ class SearchTabView {
if (lastResults2SortEvent != null)
selectedRow = resultsTable2.rowSorter.convertRowIndexToModel(selectedRow)
InfoHash infohash = model.results2[selectedRow]
Persona sender = selectedSender()
if (sender == null) // really shouldn't happen
return model.hashBucket[infohash].first()
for (UIResultEvent candidate : model.hashBucket[infohash]) {
if (candidate.sender == sender)
return candidate
}
// also shouldn't happen
return model.hashBucket[infohash].first()
} else {
int[] selectedRows = resultsTable.getSelectedRows()
@@ -492,7 +504,7 @@ class SearchTabView {
if (row < 0)
return null
if (model.groupedByFile)
return model.senders2[row].sender
return model.senders2[row]?.sender
else
return model.senders[row]
}