From 6dfddacb8d9811430f64781505dc532498559fb9 Mon Sep 17 00:00:00 2001 From: Zlatin Balevsky Date: Thu, 4 Aug 2022 14:29:12 +0100 Subject: [PATCH] wip on links to files --- .../com/muwire/gui/mulinks/FileMuLink.groovy | 78 ++++++++++ .../gui/mulinks/InvalidMuLinkException.groovy | 15 ++ .../com/muwire/gui/mulinks/MuLink.groovy | 139 ++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 gui/src/main/groovy/com/muwire/gui/mulinks/FileMuLink.groovy create mode 100644 gui/src/main/groovy/com/muwire/gui/mulinks/InvalidMuLinkException.groovy create mode 100644 gui/src/main/groovy/com/muwire/gui/mulinks/MuLink.groovy diff --git a/gui/src/main/groovy/com/muwire/gui/mulinks/FileMuLink.groovy b/gui/src/main/groovy/com/muwire/gui/mulinks/FileMuLink.groovy new file mode 100644 index 00000000..cb446e28 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/mulinks/FileMuLink.groovy @@ -0,0 +1,78 @@ +package com.muwire.gui.mulinks + +import com.muwire.core.InfoHash +import com.muwire.core.Persona +import com.muwire.core.SharedFile +import com.muwire.core.files.FileHasher +import net.i2p.crypto.DSAEngine +import net.i2p.data.SigningPrivateKey + +import java.nio.charset.StandardCharsets +import java.nio.file.Path + +class FileMuLink extends MuLink { + final long fileSize + final int pieceSizePow2 + + FileMuLink(Persona host, InfoHash infoHash, String name, byte[] sig, Map query) + throws InvalidMuLinkException { + super(host, infoHash, name, sig, LinkType.FILE) + + int fileSize, pieceSizePow2 + try { + fileSize = Long.parseLong(query.fileSize) + pieceSizePow2 = Integer.parseInt(query.pieceSizePow2) + } catch (NumberFormatException e) { + throw new InvalidMuLinkException(e) + } + + this.fileSize = fileSize + this.pieceSizePow2 = pieceSizePow2 + + if (fileSize <= 0 || fileSize > FileHasher.MAX_SIZE) + throw new InvalidMuLinkException("invalid size $fileSize") + if (pieceSizePow2 < FileHasher.MIN_PIECE_SIZE_POW2 || pieceSizePow2 > FileHasher.MAX_PIECE_SIZE_POW2) + throw new InvalidMuLinkException("invalid piece size $pieceSizePow2") + + Path path = Path.of(name) + if (path.getNameCount() != 1) + throw new InvalidMuLinkException("name path count ${path.getNameCount()}") + } + + FileMuLink(SharedFile sharedFile, Persona me, SigningPrivateKey spk) { + super(me, sharedFile.getRootInfoHash(), sharedFile.getFile().getName(), + deriveSig(sharedFile, spk), + LinkType.FILE) + fileSize = sharedFile.getCachedLength() + pieceSizePow2 = sharedFile.getPieceSize() + } + + private static byte[] deriveSig(SharedFile sharedFile, SigningPrivateKey spk) { + def baos = new ByteArrayOutputStream() + def daos = new DataOutputStream(baos) + + daos.write(sharedFile.getRoot()) + daos.write(sharedFile.getFile().getName().getBytes(StandardCharsets.UTF_8)) + daos.writeByte(LinkType.FILE.ordinal()) + daos.writeLong(sharedFile.getCachedLength()) + daos.writeByte(sharedFile.getPieceSize()) + + daos.flush() + byte [] payload = baos.toByteArray() + DSAEngine.getInstance().sign(payload, spk).getData() + } + + @Override + protected void appendSignedElements(ByteArrayOutputStream baos) { + DataOutputStream daos = new DataOutputStream(baos) + daos.writeLong(fileSize) + daos.writeByte(pieceSizePow2) + daos.flush() + } + + @Override + protected void addQueryElements(Map query) { + query.fileSize = String.valueOf(fileSize) + query.pieceSizePow2 = String.valueOf(pieceSizePow2) + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/mulinks/InvalidMuLinkException.groovy b/gui/src/main/groovy/com/muwire/gui/mulinks/InvalidMuLinkException.groovy new file mode 100644 index 00000000..7cc61646 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/mulinks/InvalidMuLinkException.groovy @@ -0,0 +1,15 @@ +package com.muwire.gui.mulinks + +class InvalidMuLinkException extends Exception { + InvalidMuLinkException(String message) { + super(message) + } + + InvalidMuLinkException(String message, Throwable cause) { + super(message, cause) + } + + InvalidMuLinkException(Throwable cause) { + super(cause) + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/mulinks/MuLink.groovy b/gui/src/main/groovy/com/muwire/gui/mulinks/MuLink.groovy new file mode 100644 index 00000000..d7497b14 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/mulinks/MuLink.groovy @@ -0,0 +1,139 @@ +package com.muwire.gui.mulinks + +import com.muwire.core.Constants +import com.muwire.core.InfoHash +import com.muwire.core.Persona +import net.i2p.crypto.DSAEngine +import net.i2p.data.Base64 +import net.i2p.data.Signature +import net.i2p.data.SigningPublicKey + +import java.nio.charset.StandardCharsets + +abstract class MuLink { + + private static final String SCHEME = "muwire" + private static final int PORT = -1 + + static enum LinkType { + FILE, + COLLECTION + } + + final String name + final Persona host + final InfoHash infoHash + final LinkType linkType + + private final byte[] sig + + protected MuLink(Persona host, InfoHash infoHash, String name, byte[] sig, LinkType linkType) { + this.host = host + this.infoHash = infoHash + this.name = name + this.sig = sig + this.linkType = linkType + } + + + boolean verify() { + ByteArrayOutputStream baos = new ByteArrayOutputStream() + + baos.write(infoHash.getRoot()) + baos.write(name.getBytes(StandardCharsets.UTF_8)) + baos.write(linkType.ordinal()) + + appendSignedElements(baos) + + byte[] payload = baos.toByteArray() + + SigningPublicKey spk = host.destination.getSigningPublicKey() + Signature signature = new Signature(spk.getType(), sig) + DSAEngine.getInstance().verifySignature(signature, payload, spk) + } + + protected abstract void appendSignedElements(ByteArrayOutputStream baos) + + String toLink() { + def query = [:] + query.name = URLEncoder.encode(name, StandardCharsets.UTF_8) + query.sig = Base64.encode(sig) + query.type = linkType.name() + addQueryElements(query) + + def kvs = [] + query.each { k, v -> + kvs << "$k=$v" + } + String queryStr = kvs.join("&") + + URI uri = new URI(SCHEME, + Base64.encode(infoHash.getRoot()), + host.toBase64(), + PORT, + "/", + queryStr, + null) + uri.toASCIIString() + } + + protected abstract void addQueryElements(Map query) + + static MuLink parse(String url) throws InvalidMuLinkException { + try { + URI uri = new URI(url) + if (uri.getScheme() != SCHEME) + throw new InvalidMuLinkException("Unsupported scheme ${uri.getScheme()}") + + if (uri.getUserInfo() == null) + throw new InvalidMuLinkException("no infohash") + InfoHash ih = new InfoHash(Base64.decode(uri.getUserInfo())) + + if (uri.getHost() == null) + throw new InvalidMuLinkException("no persona") + Persona p = new Persona(new ByteArrayInputStream(Base64.decode(uri.getHost()))) + + Map query = parseQuery(uri.getQuery()) + + if(query.name == null) + throw new InvalidMuLinkException("name missing") + String n = URLDecoder.decode(query.name, StandardCharsets.UTF_8) + + if(query.sig == null) + throw new InvalidMuLinkException("no signature") + byte[] sigBytes = Base64.decode(query.sig) + if (sigBytes.length != Constants.SIG_TYPE.getSigLen()) + throw new InvalidMuLinkException("invalid sig key") + + if (query.type == null) + throw new InvalidMuLinkException("type missing") + LinkType linkType = LinkType.valueOf(query.type) + + if (linkType == LinkType.FILE) + return new FileMuLink(p, ih, n, sigBytes, query) + throw new InvalidMuLinkException("unknown type $linkType") + } catch (InvalidMuLinkException e) { + throw e + } catch (Exception e) { + throw new InvalidMuLinkException(e) + } + } + + private static Map parseQuery(String query) throws InvalidMuLinkException { + def rv = [:] + String[] split = query.split("&") + for(String kv : split) { + int equals = kv.indexOf("=") + if (equals < 0 ) + throw new InvalidMuLinkException("invalid kv $kv") + + String k = kv.substring(0, equals) + String v = kv.substring(equals + 1, kv.length()) + + if (rv.containsKey(k)) + throw new InvalidMuLinkException("duplicate key $k") + rv.k = v + } + rv + } +}