diff --git a/core/src/main/java/com/muwire/core/Constants.java b/core/src/main/java/com/muwire/core/Constants.java index 55513a2c..5cc9bac5 100644 --- a/core/src/main/java/com/muwire/core/Constants.java +++ b/core/src/main/java/com/muwire/core/Constants.java @@ -6,6 +6,14 @@ public class Constants { public static final byte PERSONA_VERSION = (byte)1; public static final String INVALID_NICKNAME_CHARS = "'\"();<>=@$%"; public static final int MAX_NICKNAME_LENGTH = 30; + + public static final byte PROFILE_HEADER_VERSION = (byte)1; + public static final int MAX_PROFILE_TITLE_LENGTH = 128; + + public static final byte PROFILE_VERSION = (byte)1; + public static final int MAX_PROFILE_IMAGE_LENGTH = 200 * 1024; + public static final int MAX_PROFILE_LENGTH = 0x1 << 18; + public static final byte FILE_CERT_VERSION = (byte)2; public static final int CHAT_VERSION = 2; @@ -23,7 +31,6 @@ public class Constants { public static final long MAX_HEADER_TIME = 60 * 1000; public static final int MAX_RESULTS = 0x1 << 20; - public static final int MAX_PROFILE_LENGTH = 0x1 << 18; public static final int MAX_COMMENT_LENGTH = 0x1 << 15; diff --git a/core/src/main/java/com/muwire/core/InvalidSignatureException.java b/core/src/main/java/com/muwire/core/InvalidSignatureException.java index d18971ae..9e9a3d8d 100644 --- a/core/src/main/java/com/muwire/core/InvalidSignatureException.java +++ b/core/src/main/java/com/muwire/core/InvalidSignatureException.java @@ -1,6 +1,6 @@ package com.muwire.core; -class InvalidSignatureException extends Exception { +public class InvalidSignatureException extends Exception { public InvalidSignatureException(String message, Throwable cause) { super(message, cause); diff --git a/core/src/main/java/com/muwire/core/Name.java b/core/src/main/java/com/muwire/core/Name.java index beae7e29..bc43b771 100644 --- a/core/src/main/java/com/muwire/core/Name.java +++ b/core/src/main/java/com/muwire/core/Name.java @@ -13,11 +13,11 @@ import java.nio.charset.StandardCharsets; public class Name { final String name; - Name(String name) { + public Name(String name) { this.name = name; } - Name(InputStream nameStream) throws IOException { + public Name(InputStream nameStream) throws IOException { DataInputStream dis = new DataInputStream(nameStream); int length = dis.readUnsignedShort(); byte [] nameBytes = new byte[length]; diff --git a/core/src/main/java/com/muwire/core/profile/MWProfile.java b/core/src/main/java/com/muwire/core/profile/MWProfile.java new file mode 100644 index 00000000..dceb0a88 --- /dev/null +++ b/core/src/main/java/com/muwire/core/profile/MWProfile.java @@ -0,0 +1,135 @@ +package com.muwire.core.profile; + +import com.muwire.core.Constants; +import com.muwire.core.InvalidNicknameException; +import com.muwire.core.InvalidSignatureException; +import com.muwire.core.Name; +import net.i2p.crypto.DSAEngine; +import net.i2p.data.*; + +import java.io.*; + +public class MWProfile { + + private final byte version; + private final MWProfileHeader header; + private final byte[] image; + private final MWProfileImageFormat format; + private final Name body; + private final byte [] sig; + + private volatile byte[] payload; + private volatile String base64; + + public MWProfile(InputStream inputStream) throws IOException, DataFormatException, + InvalidSignatureException, InvalidNicknameException { + version = (byte) (inputStream.read() & 0xFF); + if (version != Constants.PROFILE_VERSION) + throw new IOException("unknown version " + version); + + header = new MWProfileHeader(inputStream); + + DataInputStream dais = new DataInputStream(inputStream); + + byte imageFormat = dais.readByte(); + if (imageFormat == 0) + format = MWProfileImageFormat.PNG; + else if (imageFormat == 1) + format = MWProfileImageFormat.JPG; + else + throw new IOException("unknown image format for " + header.getPersona().getHumanReadableName() + " " + imageFormat); + + int imageLength = dais.readInt(); + if (imageLength > Constants.MAX_PROFILE_IMAGE_LENGTH) + throw new IOException("image too long for " + header.getPersona().getHumanReadableName() + " " + imageLength); + image = new byte[imageLength]; + dais.readFully(image); + + body = new Name(dais); + if (body.getName().length() > Constants.MAX_COMMENT_LENGTH) + throw new IOException("body too long for " + header.getPersona().getHumanReadableName() + " " + body.getName().length()); + + sig = new byte[Constants.SIG_TYPE.getSigLen()]; + dais.readFully(sig); + + if (!verify()) + throw new InvalidSignatureException("Profile for " + header.getPersona().getHumanReadableName() + " did not verify"); + } + + public MWProfile(MWProfileHeader header, byte[] image, MWProfileImageFormat format, String body, SigningPrivateKey spk) + throws IOException, DataFormatException { + this.version = Constants.PROFILE_VERSION; + this.header = header; + this.format = format; + this.image = image; + this.body = new Name(body); + + byte [] signablePayload = signablePayload(); + Signature signature = DSAEngine.getInstance().sign(signablePayload, spk); + this.sig = signature.getData(); + } + + private byte[] signablePayload() throws IOException, DataFormatException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream daos = new DataOutputStream(baos); + + daos.write(version); + header.write(daos); + + daos.write((byte) format.ordinal()); + + daos.writeInt(image.length); + daos.write(image); + + body.write(daos); + + daos.close(); + return baos.toByteArray(); + } + + private boolean verify() throws IOException, DataFormatException { + byte [] payload = signablePayload(); + SigningPublicKey spk = header.getPersona().getDestination().getSigningPublicKey(); + Signature signature = new Signature(spk.getType(), sig); + return DSAEngine.getInstance().verifySignature(signature, payload, spk); + } + + public void write(OutputStream outputStream) throws IOException, DataFormatException { + if (payload == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(signablePayload()); + baos.write(sig); + payload = baos.toByteArray(); + } + outputStream.write(payload); + } + + public String toBase64() { + if (base64 == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + write(baos); + } catch (Exception impossible) { + throw new RuntimeException(impossible); + } + base64 = Base64.encode(baos.toByteArray()); + } + return base64; + } + + public MWProfileHeader getHeader() { + return header; + } + + public MWProfileImageFormat getFormat() { + return format; + } + + public byte[] getImage() { + return image; + } + + public String getBody() { + return body.getName(); + } +} diff --git a/core/src/main/java/com/muwire/core/profile/MWProfileHeader.java b/core/src/main/java/com/muwire/core/profile/MWProfileHeader.java new file mode 100644 index 00000000..eac0b2a6 --- /dev/null +++ b/core/src/main/java/com/muwire/core/profile/MWProfileHeader.java @@ -0,0 +1,112 @@ +package com.muwire.core.profile; + +import com.muwire.core.*; +import net.i2p.crypto.DSAEngine; +import net.i2p.data.*; + +import java.io.*; + +public class MWProfileHeader { + + private final byte version; + private final Persona persona; + private final byte[] thumbNail; + private final Name title; + private final byte[] sig; + + private volatile String base64; + private volatile byte[] payload; + + public MWProfileHeader(InputStream inputStream) throws IOException, DataFormatException, + InvalidSignatureException, InvalidNicknameException { + version = (byte) (inputStream.read() & 0xFF); + if (version != Constants.PROFILE_HEADER_VERSION) + throw new IOException("unknown version " + version); + + persona = new Persona(inputStream); + + DataInputStream dis = new DataInputStream(inputStream); + int thumbnailLength = dis.readUnsignedShort(); + thumbNail = new byte[thumbnailLength]; + dis.readFully(thumbNail); + + title = new Name(dis); + if (title.getName().length() > Constants.MAX_PROFILE_TITLE_LENGTH) + throw new IOException("Profile title too long " + title.getName().length()); + + sig = new byte[Constants.SIG_TYPE.getSigLen()]; + dis.readFully(sig); + + if (!verify()) + throw new InvalidSignatureException("Profile header for " + persona.getHumanReadableName() + " did not verify"); + } + + public MWProfileHeader(Persona persona, byte [] thumbNail, String title, SigningPrivateKey spk) + throws IOException, DataFormatException { + this.version = Constants.PROFILE_HEADER_VERSION; + this.persona = persona; + this.thumbNail = thumbNail; + this.title = new Name(title); + + byte [] signablePayload = signablePayload(); + Signature signature = DSAEngine.getInstance().sign(signablePayload, spk); + this.sig = signature.getData(); + } + + private byte[] signablePayload() throws IOException, DataFormatException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream daos = new DataOutputStream(baos); + + daos.write(version); + persona.write(daos); + daos.writeShort((short) thumbNail.length); + daos.write(thumbNail); + title.write(daos); + daos.close(); + return baos.toByteArray(); + } + + private boolean verify() throws IOException, DataFormatException { + byte [] payload = signablePayload(); + SigningPublicKey spk = persona.getDestination().getSigningPublicKey(); + Signature signature = new Signature(spk.getType(), sig); + return DSAEngine.getInstance().verifySignature(signature, payload, spk); + } + + public void write(OutputStream outputStream) throws IOException, DataFormatException { + if (payload == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream daos = new DataOutputStream(baos); + daos.write(signablePayload()); + daos.write(sig); + daos.close(); + payload = baos.toByteArray(); + } + outputStream.write(payload); + } + + public String toBase64() { + if (base64 == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + write(baos); + } catch (Exception impossible) { + throw new RuntimeException(impossible); + } + base64 = Base64.encode(baos.toByteArray()); + } + return base64; + } + + public Persona getPersona() { + return persona; + } + + public byte[] getThumbNail() { + return thumbNail; + } + + public String getTitle() { + return title.getName(); + } +} diff --git a/core/src/main/java/com/muwire/core/profile/MWProfileImageFormat.java b/core/src/main/java/com/muwire/core/profile/MWProfileImageFormat.java new file mode 100644 index 00000000..82df367b --- /dev/null +++ b/core/src/main/java/com/muwire/core/profile/MWProfileImageFormat.java @@ -0,0 +1,5 @@ +package com.muwire.core.profile; + +public enum MWProfileImageFormat { + PNG, JPG +}