From 5d51b1c580afa626abca83b9ef2c90deefc0954d Mon Sep 17 00:00:00 2001 From: Zlatin Balevsky Date: Mon, 4 Nov 2019 15:22:24 +0000 Subject: [PATCH] ability to certify shared files --- .../main/groovy/com/muwire/core/Core.groovy | 7 ++ .../muwire/core/filecert/Certificate.groovy | 119 ++++++++++++++++++ .../filecert/CertificateCreatedEvent.groovy | 7 ++ .../core/filecert/CertificateManager.groovy | 110 ++++++++++++++++ .../filecert/UICreateCertificateEvent.groovy | 8 ++ .../main/java/com/muwire/core/Constants.java | 1 + .../com/muwire/gui/MainFrameController.groovy | 8 ++ .../com/muwire/gui/MainFrameModel.groovy | 8 ++ .../views/com/muwire/gui/MainFrameView.groovy | 8 ++ 9 files changed, 276 insertions(+) create mode 100644 core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy create mode 100644 core/src/main/groovy/com/muwire/core/filecert/CertificateCreatedEvent.groovy create mode 100644 core/src/main/groovy/com/muwire/core/filecert/CertificateManager.groovy create mode 100644 core/src/main/groovy/com/muwire/core/filecert/UICreateCertificateEvent.groovy diff --git a/core/src/main/groovy/com/muwire/core/Core.groovy b/core/src/main/groovy/com/muwire/core/Core.groovy index eb9481c1..2a8a042d 100644 --- a/core/src/main/groovy/com/muwire/core/Core.groovy +++ b/core/src/main/groovy/com/muwire/core/Core.groovy @@ -18,6 +18,8 @@ import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadPausedEvent import com.muwire.core.download.UIDownloadResumedEvent +import com.muwire.core.filecert.CertificateManager +import com.muwire.core.filecert.UICreateCertificateEvent import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashingEvent @@ -99,6 +101,7 @@ public class Core { final FileManager fileManager final UploadManager uploadManager final ContentManager contentManager + final CertificateManager certificateManager private final Router router @@ -209,6 +212,10 @@ public class Core { eventBus = new EventBus() + log.info("initializing certificate manager") + certificateManager = new CertificateManager(eventBus, home, me, spk) + eventBus.register(UICreateCertificateEvent.class, certificateManager) + log.info("initializing trust service") File goodTrust = new File(home, "trusted") File badTrust = new File(home, "distrusted") diff --git a/core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy b/core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy new file mode 100644 index 00000000..860db990 --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/filecert/Certificate.groovy @@ -0,0 +1,119 @@ +package com.muwire.core.filecert + +import com.muwire.core.Constants +import com.muwire.core.InfoHash +import com.muwire.core.InvalidSignatureException +import com.muwire.core.Name +import com.muwire.core.Persona + +import net.i2p.crypto.DSAEngine +import net.i2p.data.Signature +import net.i2p.data.SigningPrivateKey +import net.i2p.data.SigningPublicKey + +class Certificate { + private final byte version + private final InfoHash infoHash + private final Name name + private final long timestamp + private final Persona issuer + private final byte[] sig + + private volatile byte [] payload + + Certificate(InputStream is) { + version = (byte) (is.read() & 0xFF) + if (version != Constants.FILE_CERT_VERSION) + throw new IOException("Unknown version $version") + + DataInputStream dis = new DataInputStream(is) + timestamp = dis.readLong() + + byte [] root = new byte[InfoHash.SIZE] + dis.readFully(root) + infoHash = new InfoHash(root) + + name = new Name(dis) + + issuer = new Persona(is) + + sig = new byte[Constants.SIG_TYPE.getSigLen()] + dis.readFully(sig) + + if (!verify(version, infoHash, name, timestamp, issuer, sig)) + throw new InvalidSignatureException("certificate for $name.name from ${issuer.getHumanReadableName()} didn't verify") + } + + Certificate(InfoHash infoHash, String name, long timestamp, Persona issuer, SigningPrivateKey spk) { + this.version = Constants.FILE_CERT_VERSION + this.infoHash = infoHash + this.name = new Name(name) + this.timestamp = timestamp + this.issuer = issuer + + ByteArrayOutputStream baos = new ByteArrayOutputStream() + DataOutputStream daos = new DataOutputStream(baos) + + daos.write(version) + daos.writeLong(timestamp) + daos.write(infoHash.getRoot()) + this.name.write(daos) + issuer.write(daos) + daos.close() + + byte[] payload = baos.toByteArray() + Signature signature = DSAEngine.getInstance().sign(payload, spk) + this.sig = signature.getData() + } + + private static boolean verify(byte version, InfoHash infoHash, Name name, long timestamp, Persona issuer, byte[] sig) { + ByteArrayOutputStream baos = new ByteArrayOutputStream() + DataOutputStream daos = new DataOutputStream(baos) + daos.write(version) + daos.writeLong(timestamp) + daos.write(infoHash.getRoot()) + name.write(daos) + issuer.write(daos) + daos.close() + + byte [] payload = baos.toByteArray() + SigningPublicKey spk = issuer.destination.getSigningPublicKey() + Signature signature = new Signature(Constants.SIG_TYPE, sig) + DSAEngine.getInstance().verifySignature(signature, payload, spk) + } + + public void write(OutputStream os) { + if (payload == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream() + DataOutputStream daos = new DataOutputStream(baos) + daos.write(version) + daos.writeLong(timestamp) + daos.write(infoHash.getRoot()) + name.write(daos) + issuer.write(daos) + daos.write(sig) + daos.close() + + payload = baos.toByteArray() + } + os.write(payload) + } + + @Override + public int hashCode() { + version.hashCode() ^ infoHash.hashCode() ^ timestamp.hashCode() ^ name.hashCode() ^ issuer.hashCode() + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Certificate)) + return false + Certificate other = (Certificate)o + + version == other.version && + infoHash == other.infoHash && + timestamp == other.timestamp && + name == other.name && + issuer == other.issuer + } +} diff --git a/core/src/main/groovy/com/muwire/core/filecert/CertificateCreatedEvent.groovy b/core/src/main/groovy/com/muwire/core/filecert/CertificateCreatedEvent.groovy new file mode 100644 index 00000000..9c5bb6bc --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/filecert/CertificateCreatedEvent.groovy @@ -0,0 +1,7 @@ +package com.muwire.core.filecert + +import com.muwire.core.Event + +class CertificateCreatedEvent extends Event { + Certificate certificate +} diff --git a/core/src/main/groovy/com/muwire/core/filecert/CertificateManager.groovy b/core/src/main/groovy/com/muwire/core/filecert/CertificateManager.groovy new file mode 100644 index 00000000..f36f1c31 --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/filecert/CertificateManager.groovy @@ -0,0 +1,110 @@ +package com.muwire.core.filecert + +import java.util.concurrent.ConcurrentHashMap +import java.util.logging.Level + +import com.muwire.core.EventBus +import com.muwire.core.InfoHash +import com.muwire.core.InvalidSignatureException +import com.muwire.core.Name +import com.muwire.core.Persona + +import groovy.util.logging.Log +import net.i2p.data.Base64 +import net.i2p.data.SigningPrivateKey + +@Log +class CertificateManager { + + private final EventBus eventBus + private final File certDir + private final Persona me + private final SigningPrivateKey spk + + final Map> byInfoHash = new ConcurrentHashMap() + final Map> byIssuer = new ConcurrentHashMap() + + CertificateManager(EventBus eventBus, File home, Persona me, SigningPrivateKey spk) { + this.eventBus = eventBus + this.me = me + this.spk = spk + this.certDir = new File(home, "filecerts") + if (!certDir.exists()) + certDir.mkdirs() + else + loadCertificates() + } + + private void loadCertificates() { + certDir.listFiles({ dir, name -> + name.endsWith("mwcert") + } as FilenameFilter).each { certFile -> + Certificate cert = null + try { + certFile.withInputStream { + cert = new Certificate(it) + } + } catch (IOException | InvalidSignatureException ignore) { + log.log(Level.WARNING, "Certificate failed to load from $certFile", ignore) + return + } + + Set existing = byInfoHash.get(cert.infoHash) + if (existing == null) { + existing = new HashSet<>() + byInfoHash.put(cert.infoHash, existing) + } + existing.add(cert) + + existing = byIssuer.get(cert.issuer) + if (existing == null) { + existing = new HashSet<>() + byIssuer.put(cert.issuer, existing) + } + existing.add(cert) + + eventBus.publish(new CertificateCreatedEvent(certificate : cert)) + } + } + + void onUICreateCertificateEvent(UICreateCertificateEvent e) { + InfoHash infoHash = e.sharedFile.getInfoHash() + String name = e.sharedFile.getFile().getName() + long timestamp = System.currentTimeMillis() + Certificate cert = new Certificate(infoHash, name, timestamp, me, spk) + + boolean added = true + + Set existing = byInfoHash.get(cert.infoHash) + if (existing == null) { + existing = new HashSet<>() + byInfoHash.put(cert.infoHash, existing) + } + added &= existing.add(cert) + + existing = byIssuer.get(cert.issuer) + if (existing == null) { + existing = new HashSet<>() + byIssuer.put(cert.issuer, existing) + } + added &= existing.add(cert) + + if (added) { + String infoHashString = Base64.encode(infoHash.getRoot()) + File certFile = new File(certDir, "${infoHashString}${name}.mwcert") + certFile.withOutputStream { cert.write(it) } + eventBus.publish(new CertificateCreatedEvent(certificate : cert)) + } + } + + boolean hasLocalCertificate(InfoHash infoHash) { + if (!byInfoHash.containsKey(infoHash)) + return false + Set set = byInfoHash.get(infoHash) + for (Certificate cert : set) { + if (cert.issuer == me) + return true + } + return false + } +} diff --git a/core/src/main/groovy/com/muwire/core/filecert/UICreateCertificateEvent.groovy b/core/src/main/groovy/com/muwire/core/filecert/UICreateCertificateEvent.groovy new file mode 100644 index 00000000..4da49877 --- /dev/null +++ b/core/src/main/groovy/com/muwire/core/filecert/UICreateCertificateEvent.groovy @@ -0,0 +1,8 @@ +package com.muwire.core.filecert + +import com.muwire.core.Event +import com.muwire.core.SharedFile + +class UICreateCertificateEvent extends Event { + SharedFile sharedFile +} diff --git a/core/src/main/java/com/muwire/core/Constants.java b/core/src/main/java/com/muwire/core/Constants.java index d8abe80c..e2101b0b 100644 --- a/core/src/main/java/com/muwire/core/Constants.java +++ b/core/src/main/java/com/muwire/core/Constants.java @@ -4,6 +4,7 @@ import net.i2p.crypto.SigType; public class Constants { public static final byte PERSONA_VERSION = (byte)1; + public static final byte FILE_CERT_VERSION = (byte)1; public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519; public static final int MAX_HEADER_SIZE = 0x1 << 14; diff --git a/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy b/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy index 7df909e2..d846d6fa 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/MainFrameController.groovy @@ -28,6 +28,7 @@ import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadPausedEvent import com.muwire.core.download.UIDownloadResumedEvent +import com.muwire.core.filecert.UICreateCertificateEvent import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.UIPersistFilesEvent @@ -338,6 +339,13 @@ class MainFrameController { performSearch(it) } } + + @ControllerAction + void issueCertificate() { + view.selectedSharedFiles().each { + core.eventBus.publish(new UICreateCertificateEvent(sharedFile : it)) + } + } void saveMuWireSettings() { core.saveMuSettings() diff --git a/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy b/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy index 74345c30..ee82ff98 100644 --- a/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy @@ -27,6 +27,7 @@ import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.content.ContentControlEvent import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.Downloader +import com.muwire.core.filecert.CertificateCreatedEvent import com.muwire.core.files.AllFilesLoadedEvent import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryWatchedEvent @@ -208,6 +209,7 @@ class MainFrameModel { core.eventBus.register(UpdateDownloadedEvent.class, this) core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this) core.eventBus.register(SearchEvent.class, this) + core.eventBus.register(CertificateCreatedEvent.class, this) core.muOptions.watchedKeywords.each { core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true)) @@ -571,6 +573,12 @@ class MainFrameModel { } } + void onCertificateCreatedEvent(CertificateCreatedEvent e) { + runInsideUIAsync { + view.refreshSharedFiles() + } + } + private void insertIntoTree(SharedFile file) { List parents = new ArrayList<>() File tmp = file.file.getParentFile() diff --git a/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy b/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy index 1db3ed72..8f1438b4 100644 --- a/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy @@ -269,6 +269,10 @@ class MainFrameView { closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()}) closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.getCachedLength() }) closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null}) + closureColumn(header : "Certificates", preferredWidth : 50, type : Boolean, read : { + Core core = application.context.get("core") + core.certificateManager.hasLocalCertificate(it.getInfoHash()) + }) closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()}) closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()}) } @@ -295,6 +299,7 @@ class MainFrameView { panel { button(text : "Share files", actionPerformed : shareFiles) button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction) + button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, issueCertificateAction) } panel { panel { @@ -596,6 +601,9 @@ class MainFrameView { JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files") commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()}) sharedFilesMenu.add(commentSelectedFiles) + JMenuItem certifySelectedFiles = new JMenuItem("Certify selected files") + certifySelectedFiles.addActionListener({mvcGroup.controller.issueCertificate()}) + sharedFilesMenu.add(certifySelectedFiles) def sharedFilesMouseListener = new MouseAdapter() { @Override