From 57067db6ae768ff3fd8d611be2111c402f0c1e25 Mon Sep 17 00:00:00 2001 From: Jack Schmidt Date: Tue, 28 Feb 2017 01:17:08 -0500 Subject: [PATCH 1/2] Add a JSON receive logger --- build.gradle | 1 + .../signal/JsonReceiveMessageHandler.java | 270 ++++++++++++++++++ src/main/java/org/asamk/signal/Main.java | 46 +++ 3 files changed, 317 insertions(+) create mode 100644 src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java diff --git a/build.gradle b/build.gradle index 346c13e7..16e79cb5 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ dependencies { compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' + compile 'org.json:json:20160810' } jar { diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java new file mode 100644 index 00000000..61bf4339 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -0,0 +1,270 @@ +package org.asamk.signal; + +import org.asamk.signal.util.Base64; +import org.json.JSONObject; +import org.json.JSONArray; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.messages.calls.*; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { + private static final TimeZone tzUTC = TimeZone.getTimeZone("UTC"); + private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + final Manager m; + final Writer logfile; + + public JsonReceiveMessageHandler(Manager m, Writer logfile) { + this.m = m; + this.logfile = logfile; + df.setTimeZone(tzUTC); + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + final JSONObject obj = new JSONObject(); + + obj.put( "envelope", jsonEnvelope( envelope ) ); + + if (exception != null) { + if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { + org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) exception; + final JSONObject err = new JSONObject(); + err.put("type", "UntrustedIdentityException"); + err.put("name", e.getName()); + obj.put("error", err); + } else { + final JSONObject err = new JSONObject(); + err.put("type", exception.getClass().getSimpleName()); + err.put("message", exception.getMessage() ); + obj.put("error", err); + } + } + + if (content == null) { + if( envelope.hasContent() && exception == null ) { + final JSONObject err = new JSONObject(); + err.put("type", exception.getClass().getSimpleName()); + err.put("message", "Failed to decrypt message"); + obj.put("error", err); + } + } else { + if (content.getDataMessage().isPresent()) + obj.put( "data", jsonDataMessage( content.getDataMessage().get() ) ); + if (content.getSyncMessage().isPresent()) + obj.put( "sync", jsonSyncMessage( content.getSyncMessage().get() ) ); + if (content.getCallMessage().isPresent()) + obj.put( "call", jsonCallMessage( content.getCallMessage().get() ) ); + } + + try { + logfile.write(obj.toString()); + logfile.write(",\n"); + logfile.flush(); + } catch(IOException e) { + e.printStackTrace(); + } + } + + private static final JSONObject jsonEnvelope( SignalServiceEnvelope envelope ) { + final JSONObject env = new JSONObject(); + env.put("timestamp", formatTimestamp(envelope.getTimestamp())); + final JSONObject from = new JSONObject(); + from.put("number", envelope.getSource()); + from.put("device", envelope.getSourceDevice()); + SignalServiceAddress sourceAddress = envelope.getSourceAddress(); + if( sourceAddress.getRelay().isPresent() ) from.put("relay", envelope.getRelay() ); + env.put("from", from); + final JSONObject type = new JSONObject(); + type.put("number", envelope.getType()); + if(envelope.isReceipt()) type.put("name", "receipt"); + if(envelope.isSignalMessage()) type.put("name", "message"); + if(envelope.isPreKeySignalMessage()) type.put("name", "prekey"); + env.put("type", type); + if(envelope.hasLegacyMessage()) env.put("legacyMessage", true ); + return env; + } + + private final JSONObject jsonAttachment(SignalServiceAttachment attachment ) { + final JSONObject json = new JSONObject(); + json.put("content_type", attachment.getContentType() ); + json.put("type", (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") ); + if (attachment.isPointer()) { + final SignalServiceAttachmentPointer pointer = attachment.asPointer(); + json.put("id", pointer.getId() ); + json.put("key", Base64.encodeBytes( pointer.getKey() ) ); + if( pointer.getRelay().isPresent() ) json.put("relay", pointer.getRelay().get() ); + if( pointer.getSize().isPresent() ) json.put("size", pointer.getSize().get()); + if( pointer.getPreview().isPresent() ) json.put("preview", Base64.encodeBytes( pointer.getPreview().get() ) ); + // Added 2017-02-25, version 2.5.2 + //if( pointer.getDigest().isPresent() ) json.put("digest", Base64.encodeBytes( pointer.getDigest().get() ) ); + final File file = m.getAttachmentFile(pointer.getId()); + if (file.exists()) json.put("file", file.toString() ); + } + return json; + } + + private final JSONObject jsonGroup( SignalServiceGroup ssg ) { + final JSONObject group = new JSONObject(); + group.put( "id", Base64.encodeBytes( ssg.getGroupId() ) ); + group.put( "type", ssg.getType() ); + if( ssg.getName().isPresent() ) group.put( "name", ssg.getName().get() ); + if( ssg.getMembers().isPresent() ) { + final JSONArray members = new JSONArray(); + for( String member : ssg.getMembers().get()) { + members.put(member); + } + group.put("members", members); + } + if( ssg.getAvatar().isPresent() ) { + group.put("avatar", jsonAttachment( ssg.getAvatar().get() ) ); + } + return group; + } + + private static final JSONArray jsonListOfStrings( List membersList ) { + final JSONArray members = new JSONArray(); + for( final String member : membersList ) { + members.put( member ); + } + return members; + } + + private final JSONObject jsonDataMessage( SignalServiceDataMessage message ) { + final JSONObject data = new JSONObject(); + data.put("timestamp", message.getTimestamp()); + if( message.getGroupInfo().isPresent() ) + data.put( "group", jsonGroup( message.getGroupInfo().get() ) ); + if( message.getBody().isPresent() ) + data.put("body", message.getBody().get()); + if( message.isEndSession() ) + data.put("isEndSession", true); + if( message.isExpirationUpdate() ) + data.put("isExpirationUpdate", true); + if( message.isGroupUpdate() ) + data.put("isGroupUpdate", true ); + if( message.getExpiresInSeconds() > 0) + data.put("expiresInSeconds", message.getExpiresInSeconds()); + if( message.getAttachments().isPresent() ) { + final JSONArray attachments = new JSONArray(); + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + attachments.put( jsonAttachment( attachment ) ); + } + data.put("attachments", attachments ); + } + return data; + } + + private final JSONObject jsonSyncMessage( SignalServiceSyncMessage syncMessage ) { + final JSONObject sync = new JSONObject(); + if (syncMessage.getGroups().isPresent()) { + sync.put("groups", jsonAttachment( syncMessage.getGroups().get() ) ); + } + if (syncMessage.getContacts().isPresent()) { + sync.put("contacts", jsonAttachment( syncMessage.getContacts().get() ) ); + } + if (syncMessage.getRead().isPresent()) { + final JSONArray read = new JSONArray(); + for (ReadMessage rm : syncMessage.getRead().get()) { + final JSONObject mesg = new JSONObject(); + mesg.put("from", rm.getSender()); + mesg.put("timestamp", rm.getTimestamp()); + read.put(mesg); + } + sync.put("read", read); + } + if (syncMessage.getRequest().isPresent()) { + final RequestMessage requestMessage = syncMessage.getRequest().get(); + final JSONObject request = new JSONObject(); + if (requestMessage.isContactsRequest()) { + request.put("contacts", true); + } + if (requestMessage.isGroupsRequest()) { + request.put("groups", true); + } + if (requestMessage.isBlockedListRequest()) { + request.put("blockedNumbers", true); + } + sync.put("request", request); + } + if (syncMessage.getSent().isPresent()) { + final JSONObject sent = new JSONObject(); + final SentTranscriptMessage sentTranscriptMessage = syncMessage.getSent().get(); + sent.put("timestamp", formatTimestamp(sentTranscriptMessage.getTimestamp())); + if (sentTranscriptMessage.getDestination().isPresent()) + sent.put("dest", sentTranscriptMessage.getDestination().get() ); + if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) + sent.put("burntime", formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp())); + sent.put("data", jsonDataMessage( sentTranscriptMessage.getMessage() ) ); + sync.put("sent", sent); + } + if (syncMessage.getBlockedList().isPresent()) { + final JSONArray blockedNumbers = new JSONArray(); + final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); + for (final String number : blockedList.getNumbers()) { + blockedNumbers.put( number ); + } + sync.put("blockedNumbers", blockedNumbers); + } + return sync; + } + + private static final JSONObject jsonCallMessage( SignalServiceCallMessage ssCall ) { + final JSONObject call = new JSONObject(); + if( ssCall.getOfferMessage().isPresent() ) { + final OfferMessage offerMessage = ssCall.getOfferMessage().get(); + final JSONObject offer = new JSONObject(); + offer.put( "id", offerMessage.getId() ); + offer.put( "description", offerMessage.getDescription() ); + call.put( "offer", offer ); + } + if( ssCall.getAnswerMessage().isPresent() ) { + final AnswerMessage answerMessage = ssCall.getAnswerMessage().get(); + final JSONObject answer = new JSONObject(); + answer.put( "id", answerMessage.getId() ); + answer.put( "description", answerMessage.getDescription() ); + call.put( "answer", answer ); + } + if( ssCall.getHangupMessage().isPresent() ) { + final HangupMessage hangupMessage = ssCall.getHangupMessage().get(); + final JSONObject hangup = new JSONObject(); + hangup.put( "id", hangupMessage.getId() ); + call.put( "hangup", hangup ); + } + if( ssCall.getBusyMessage().isPresent() ) { + final BusyMessage busyMessage = ssCall.getBusyMessage().get(); + final JSONObject busy = new JSONObject(); + busy.put( "id", busyMessage.getId() ); + call.put( "busy", busy ); + } + if( ssCall.getIceUpdateMessages().isPresent() ) { + final JSONArray ices = new JSONArray(); + for( final IceUpdateMessage iceMessage : ssCall.getIceUpdateMessages().get()) { + final JSONObject ice = new JSONObject(); + ice.put( "id", iceMessage.getId() ); + ice.put( "sdp", iceMessage.getSdp() ); + ice.put( "sdpMLineIndex", iceMessage.getSdpMLineIndex() ); + ice.put( "sdpMid", iceMessage.getSdpMid() ); + ices.put(ice); + } + call.put( "ices", ices ); + } + return call; + } + + private static final String formatTimestamp(long timestamp) { + final Date date = new Date(timestamp); + return df.format(date); + } + +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index f9084516..5732cdb8 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -26,6 +26,7 @@ import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.Base64; import org.asamk.signal.util.Hex; +import org.asamk.signal.JsonReceiveMessageHandler; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; @@ -41,9 +42,11 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; +import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; @@ -368,6 +371,39 @@ public class Main { } } + break; + case "json": + { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + double timeout = 5; + if (ns.getDouble("timeout") != null) { + timeout = ns.getDouble("timeout"); + } + boolean returnOnTimeout = true; + if (timeout < 0) { + returnOnTimeout = false; + timeout = 3600; + } + boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); + try { + Writer logfile = new FileWriter(ns.getString("logfile"),true); + logfile.write("{\"init\":true},\n"); + logfile.flush(); + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, new JsonReceiveMessageHandler(m,logfile)); + logfile.write("{\"done\":true}]\n"); + logfile.flush(); + logfile.close(); + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } + } break; case "receive": if (dBusConn != null) { @@ -804,6 +840,16 @@ public class Main { mutTrust.addArgument("-v", "--verified-fingerprint") .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint."); + Subparser parserJson = subparsers.addParser("json"); + parserJson.addArgument("-t", "--timeout") + .type(double.class) + .help("Number of seconds to wait for new messages (negative values disable timeout)"); + parserJson.addArgument("--ignore-attachments") + .help("Don’t download attachments of received messages.") + .action(Arguments.storeTrue()); + parserJson.addArgument("-l","--logfile") + .help("File to store received messages in JSON format."); + Subparser parserReceive = subparsers.addParser("receive"); parserReceive.addArgument("-t", "--timeout") .type(double.class) From 669801edbe685ceff01041869ed79390ddd926d2 Mon Sep 17 00:00:00 2001 From: Jack Schmidt Date: Tue, 28 Feb 2017 01:21:12 -0500 Subject: [PATCH 2/2] include decoding contact and group attachments. requires more access to the Manager --- .../signal/JsonReceiveMessageHandler.java | 68 +++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 6 +- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 61bf4339..d05ee890 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -1,6 +1,7 @@ package org.asamk.signal; import org.asamk.signal.util.Base64; +import org.asamk.signal.util.Util; import org.json.JSONObject; import org.json.JSONArray; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -12,6 +13,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.File; import java.io.IOException; import java.io.Writer; +import java.nio.file.Files; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -267,4 +269,70 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler return df.format(date); } + private final JSONArray jsonGroupsAttachment( SignalServiceAttachment groupsAttachment ) { + final JSONArray groups = new JSONArray(); + File tmpFile = null; + try { + tmpFile = Util.createTempFile(); + final DeviceGroupsInputStream s = new DeviceGroupsInputStream( + m.retrieveAttachmentAsStream(groupsAttachment.asPointer(), tmpFile ) ); + DeviceGroup g; + while ((g = s.read()) != null) { + final JSONObject group = new JSONObject(); + group.put("id", Base64.encodeBytes( g.getId() ) ); + group.put("isActive", g.isActive() ); + group.put("members", jsonListOfStrings( g.getMembers() ) ); + if(g.getName().isPresent()) + group.put("name", g.getName().get() ); + if (g.getAvatar().isPresent()) // group.put("avatar", jsonAttachment( g.getAvatar().get() ) ); + m.retrieveGroupAvatarAttachment(g.getAvatar().get(), g.getId() ); + groups.put( group ); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (tmpFile != null) { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + } + } + } + return groups; + } + + private final JSONArray jsonContactsAttachment( SignalServiceAttachment contactsAttachment ) { + final JSONArray contacts = new JSONArray(); + File tmpFile = null; + try { + tmpFile = Util.createTempFile(); + final DeviceContactsInputStream s = new DeviceContactsInputStream( + m.retrieveAttachmentAsStream(contactsAttachment.asPointer(), tmpFile)); + DeviceContact c; + while ((c = s.read()) != null) { + final JSONObject contact = new JSONObject(); + contact.put( "number", c.getNumber() ); + if( c.getName().isPresent() ) + contact.put( "name", c.getName().get() ); + if( c.getColor().isPresent() ) + contact.put( "color", c.getColor().get() ); + if( c.getAvatar().isPresent() ) //contact.put( "avatar", jsonAttachment( c.getAvatar().get() ) ); + m.retrieveContactAvatarAttachment(c.getAvatar().get(), c.getNumber() ); + contacts.put( contact ); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (tmpFile != null) { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + } + } + } + return contacts; + } + } diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index e817c0d0..07fba701 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1340,7 +1340,7 @@ class Manager implements Signal { return new File(avatarsPath, "contact-" + number); } - private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { + public File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); @@ -1355,7 +1355,7 @@ class Manager implements Signal { return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); } - private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { + public File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); @@ -1429,7 +1429,7 @@ class Manager implements Signal { return outputFile; } - private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { + public InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); return messageReceiver.retrieveAttachment(pointer, tmpFile); }